Compare commits
326 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a99e6ba071 | ||
|
|
604ee02cd9 | ||
|
|
926a48a65b | ||
|
|
98375dc902 | ||
|
|
e73de332a1 | ||
|
|
b28b2d05bd | ||
|
|
9e5f031553 | ||
|
|
dab5ba363c | ||
|
|
e42387d0da | ||
|
|
730a03a3b2 | ||
|
|
7d195b97c2 | ||
|
|
4fb2dba587 | ||
|
|
76697280c9 | ||
|
|
0df6ac6140 | ||
|
|
5453b71fd1 | ||
|
|
1f3070c882 | ||
|
|
3b7e7a7f56 | ||
|
|
06a8eb115c | ||
|
|
e4f0a470e9 | ||
|
|
adee0b8ccb | ||
|
|
0bebfe454e | ||
|
|
84b0c3df4f | ||
|
|
764bd556f3 | ||
|
|
069c7c9d35 | ||
|
|
393ce05860 | ||
|
|
cf78b86cb5 | ||
|
|
4f03d7733a | ||
|
|
e3a14d546a | ||
|
|
f2007f4d95 | ||
|
|
8969464b00 | ||
|
|
6137d66914 | ||
|
|
6fbe660f96 | ||
|
|
74320f0075 | ||
|
|
bfad972f0c | ||
|
|
bb918b579a | ||
|
|
e145090046 | ||
|
|
70b5c822bb | ||
|
|
f2df77a4f1 | ||
|
|
8d416634ba | ||
|
|
9f4433d8b5 | ||
|
|
2d8f7d2a7b | ||
|
|
a9fbc9eda1 | ||
|
|
e092da5f78 | ||
|
|
e42e7e5cbd | ||
|
|
93fac1f312 | ||
|
|
d5504fa5d0 | ||
|
|
273aba38d4 | ||
|
|
cab0aa462c | ||
|
|
b03e2270a0 | ||
|
|
21049be233 | ||
|
|
f89c47b83d | ||
|
|
44f1f22068 | ||
|
|
a229547048 | ||
|
|
4f700c23ba | ||
|
|
b69fc19b35 | ||
|
|
cd1d1996df | ||
|
|
963fcc1444 | ||
|
|
c6825e3d0d | ||
|
|
20bdba15f6 | ||
|
|
e636857057 | ||
|
|
1ae8523098 | ||
|
|
8eb802d3a0 | ||
|
|
6fc031c523 | ||
|
|
8c93289a72 | ||
|
|
b1df0fafa2 | ||
|
|
15046a0454 | ||
|
|
fb9b6314a0 | ||
|
|
0719a3e36e | ||
|
|
a3b0efb82e | ||
|
|
bde324820d | ||
|
|
bbdbbd0b1b | ||
|
|
d4f3b292e6 | ||
|
|
39eb937830 | ||
|
|
fbab5bd444 | ||
|
|
12ca922a41 | ||
|
|
f4e552f982 | ||
|
|
94d26d00ba | ||
|
|
d80d1f8493 | ||
|
|
ace4350319 | ||
|
|
4441cf1045 | ||
|
|
cf99b47ec0 | ||
|
|
546eb50bac | ||
|
|
5e094c8a7c | ||
|
|
c683f2c96c | ||
|
|
e5a372fa2d | ||
|
|
02f45b679f | ||
|
|
b1cda65dcf | ||
|
|
74ce65d9ff | ||
|
|
ccebe5e069 | ||
|
|
b6ec667de0 | ||
|
|
390b7ddc5e | ||
|
|
38739b16bc | ||
|
|
27525f1d42 | ||
|
|
43a9dc48e0 | ||
|
|
440eb387d7 | ||
|
|
28ffaf9348 | ||
|
|
d7da6dde0e | ||
|
|
e000620cdf | ||
|
|
f09309485a | ||
|
|
e04e2ebab5 | ||
|
|
91a107eb6f | ||
|
|
5ce9e0193a | ||
|
|
4c71c26593 | ||
|
|
abdd2455bb | ||
|
|
c33f8d2790 | ||
|
|
8e9d08bc10 | ||
|
|
9593129e6a | ||
|
|
267da3b4db | ||
|
|
c9ded489c9 | ||
|
|
4c73d070ac | ||
|
|
121b9d0715 | ||
|
|
fbb33b7abc | ||
|
|
7178bab6b4 | ||
|
|
2d7452bfaa | ||
|
|
b0f3bfef27 | ||
|
|
7bc6dc5cf3 | ||
|
|
ee7b634dce | ||
|
|
b0bd752180 | ||
|
|
4d14af5d4b | ||
|
|
7953e58c74 | ||
|
|
549d73a0b1 | ||
|
|
8301bba8ad | ||
|
|
78f17aa541 | ||
|
|
7578a7466f | ||
|
|
8681a6b4e2 | ||
|
|
efed313721 | ||
|
|
795cf39ddf | ||
|
|
f08f248cb7 | ||
|
|
3c20425649 | ||
|
|
dfc689411b | ||
|
|
2295407a45 | ||
|
|
828a2acd26 | ||
|
|
843b8ceab0 | ||
|
|
011451464f | ||
|
|
32d170621c | ||
|
|
464d022a86 | ||
|
|
6a0066253f | ||
|
|
d627b3bfc8 | ||
|
|
952c62df37 | ||
|
|
b6cc1c9492 | ||
|
|
39ae122304 | ||
|
|
c34c6926d5 | ||
|
|
4fe512ff3a | ||
|
|
4197921465 | ||
|
|
4b69ab08c1 | ||
|
|
f3a0058eb9 | ||
|
|
633b6f596d | ||
|
|
e6274c0757 | ||
|
|
0898a7bb57 | ||
|
|
fafd5234bd | ||
|
|
8cb10f76e4 | ||
|
|
f1d7f59e49 | ||
|
|
bc9a99387f | ||
|
|
5289d49f75 | ||
|
|
69e9f6d29d | ||
|
|
0b42437052 | ||
|
|
ae0f750770 | ||
|
|
9fe7e0d63d | ||
|
|
8935794e28 | ||
|
|
d44ff447bd | ||
|
|
798d3e2d54 | ||
|
|
e8f99c3326 | ||
|
|
1a5f380c00 | ||
|
|
b4827a98ca | ||
|
|
3ea5e4d4b2 | ||
|
|
5f77ac8d6f | ||
|
|
5d0cf3d919 | ||
|
|
4b1da0cf3c | ||
|
|
862ced3bd0 | ||
|
|
79b256a0fa | ||
|
|
0d6ff7d1b7 | ||
|
|
ecc5fe24a9 | ||
|
|
1fb2317bac | ||
|
|
6246eb9717 | ||
|
|
8f763c42b6 | ||
|
|
6472bda29e | ||
|
|
c0cad91cb6 | ||
|
|
1149dea4b2 | ||
|
|
6a6024e38f | ||
|
|
8901d11674 | ||
|
|
8b7f7cbc30 | ||
|
|
b6d0bdfa2d | ||
|
|
44896bcd51 | ||
|
|
bdf2b2d5c4 | ||
|
|
035726f650 | ||
|
|
1abb3cd566 | ||
|
|
f7772f00c4 | ||
|
|
216b5341ae | ||
|
|
eeeef9ca86 | ||
|
|
cc9293b386 | ||
|
|
efe43077bc | ||
|
|
949c7726d1 | ||
|
|
0b7bda291c | ||
|
|
872cf0d726 | ||
|
|
af09223dd5 | ||
|
|
7d62f103e4 | ||
|
|
9e85d37fb9 | ||
|
|
8dee06f83a | ||
|
|
82fe4aa6c0 | ||
|
|
50c169e0a3 | ||
|
|
7364525bf5 | ||
|
|
54910fdb76 | ||
|
|
332a3c4cbf | ||
|
|
ac41c41809 | ||
|
|
96a9df04ed | ||
|
|
b7cc4158d5 | ||
|
|
2bbe6269cd | ||
|
|
eb54189683 | ||
|
|
e8e59306fc | ||
|
|
8af3fe3b4a | ||
|
|
3103247e8f | ||
|
|
1629a7d280 | ||
|
|
b5a5169372 | ||
|
|
4b4bfae4f4 | ||
|
|
d5639e6e95 | ||
|
|
9e67f74ca3 | ||
|
|
e3ddfbf2b8 | ||
|
|
1ea78c7ae7 | ||
|
|
e7af3bf55d | ||
|
|
e52cec9cdf | ||
|
|
5bb48b51a0 | ||
|
|
d2e1b35eee | ||
|
|
ef204b0adf | ||
|
|
f742434043 | ||
|
|
d3b34ce323 | ||
|
|
89c2f4f2ff | ||
|
|
5a0f23e6d6 | ||
|
|
5e05e8b62b | ||
|
|
1f7273af23 | ||
|
|
2b8302bced | ||
|
|
1b94462410 | ||
|
|
120bb443fe | ||
|
|
6fc3c03c4b | ||
|
|
46b79c7c61 | ||
|
|
4782d8aa1f | ||
|
|
fe4e305410 | ||
|
|
040c1fc302 | ||
|
|
5edea5a8dc | ||
|
|
d2b65537f6 | ||
|
|
1183f68e19 | ||
|
|
da6fe01eca | ||
|
|
c27cea6f30 | ||
|
|
cd0532b4d6 | ||
|
|
c9de6c003b | ||
|
|
418621a9ff | ||
|
|
f871724ae6 | ||
|
|
def68ddc8f | ||
|
|
a31db3df9c | ||
|
|
64217a8a5b | ||
|
|
79079b54ea | ||
|
|
77a7619690 | ||
|
|
9f2d7adb8e | ||
|
|
07dd9c6bc8 | ||
|
|
45939171ea | ||
|
|
049849264e | ||
|
|
7e0d48c2a1 | ||
|
|
ad1468f66f | ||
|
|
058bcddc53 | ||
|
|
8288de0c84 | ||
|
|
1da2afd450 | ||
|
|
03de51747e | ||
|
|
3d698cd7c1 | ||
|
|
a48cc245e7 | ||
|
|
9ed3a8ee05 | ||
|
|
64daf1310d | ||
|
|
e5ba0d9d9c | ||
|
|
50e4e9d58d | ||
|
|
03b9db5e0a | ||
|
|
043cb2ea44 | ||
|
|
a62d70fbd5 | ||
|
|
053e80a08e | ||
|
|
b726dcc770 | ||
|
|
9df133ed8c | ||
|
|
50dd7b00c3 | ||
|
|
ccbd2c924b | ||
|
|
52d5c3beeb | ||
|
|
c43416891e | ||
|
|
56a573de86 | ||
|
|
e7fff2529c | ||
|
|
78867647d1 | ||
|
|
09f32d4f84 | ||
|
|
6f0f70bd92 | ||
|
|
6df15ddf6e | ||
|
|
922c0887f1 | ||
|
|
d7c9243880 | ||
|
|
f42a595aba | ||
|
|
797722ec12 | ||
|
|
bb4bf23c5c | ||
|
|
f3aacbd253 | ||
|
|
106fce26b5 | ||
|
|
caf208b0a4 | ||
|
|
13b9a8bc9a | ||
|
|
14ce230683 | ||
|
|
f31fbc10f6 | ||
|
|
be404068ff | ||
|
|
5671ec5f58 | ||
|
|
da3b0bf7c8 | ||
|
|
90ade3225f | ||
|
|
4928d1d490 | ||
|
|
9c52eb9d6f | ||
|
|
0a58cb2877 | ||
|
|
7581830e70 | ||
|
|
d468866746 | ||
|
|
999e170f1d | ||
|
|
7513bfb13a | ||
|
|
1f27002b84 | ||
|
|
669bfe763a | ||
|
|
860370a845 | ||
|
|
196761a40a | ||
|
|
26d5444919 | ||
|
|
e05c41828c | ||
|
|
c4cce58464 | ||
|
|
f7e6d4e724 | ||
|
|
d02e992265 | ||
|
|
3e13936e08 | ||
|
|
a3dfcd5a95 | ||
|
|
ce928dc6c8 | ||
|
|
1dea988cd6 | ||
|
|
74bb6f0012 | ||
|
|
79888d3bde | ||
|
|
4e1d3e45a3 | ||
|
|
682db77401 | ||
|
|
6faed08d9d | ||
|
|
62b200a4be | ||
|
|
f7bab5fdc0 | ||
|
|
5ff0ac2816 |
17
.github/workflows/cd.yml
vendored
17
.github/workflows/cd.yml
vendored
@@ -6,8 +6,8 @@ on:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
cd:
|
||||
runs-on: ubuntu-20.04
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
@@ -16,25 +16,34 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.14.x
|
||||
go-version: 1.16.x
|
||||
- name: Run goreleaser
|
||||
uses: goreleaser/goreleaser-action@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_API_TOKEN}}
|
||||
- name: Bump Homebrew
|
||||
homebrew:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Bump Homebrew formula
|
||||
uses: dawidd6/action-homebrew-bump-formula@v3
|
||||
with:
|
||||
token: ${{secrets.GITHUB_API_TOKEN}}
|
||||
formula: lazygit
|
||||
ppa:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout PPA repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: dawidd6/lazygit-debian
|
||||
token: ${{secrets.GITHUB_API_TOKEN}}
|
||||
fetch-depth: 0
|
||||
- name: Setup git
|
||||
uses: dawidd6/action-git-user-config@v1
|
||||
- name: Update PPA repo
|
||||
run: |
|
||||
version="$(echo "$GITHUB_REF" | sed 's@refs/tags/v@@')"
|
||||
sudo apt update
|
||||
sudo apt install -y git-buildpackage
|
||||
git fetch --tags https://github.com/$GITHUB_REPOSITORY
|
||||
gbp import-ref -u "$version"
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.14.x
|
||||
go-version: 1.16.x
|
||||
- name: Cache build
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
|
||||
14
.github/workflows/lint.yml
vendored
Normal file
14
.github/workflows/lint.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
name: Lint
|
||||
|
||||
on: pull_request
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Lint
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
with:
|
||||
version: latest
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -31,3 +31,4 @@ test/integration/*/used_config/
|
||||
# these sample hooks waste too space space
|
||||
test/integration/*/expected/.git_keep/hooks/
|
||||
!.git_keep/
|
||||
lazygit.exe
|
||||
|
||||
23
README.md
23
README.md
@@ -30,6 +30,7 @@ If you're a mere mortal like me and you're tired of hearing how powerful git is
|
||||
- [FreeBSD](#freebsd)
|
||||
- [Conda](#conda)
|
||||
- [Go](#go)
|
||||
- [Chocolatey (Windows)](#chocolatey-windows)
|
||||
- [Manual](#manual)
|
||||
- [Usage](#usage)
|
||||
- [Keybindings](#keybindings)
|
||||
@@ -52,7 +53,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(10.10+) or Linux, you can download a binary release [here](../../releases).
|
||||
|
||||
### Homebrew
|
||||
|
||||
@@ -113,12 +114,12 @@ scoop install lazygit
|
||||
|
||||
### Arch Linux
|
||||
|
||||
Packages for Arch Linux are available via AUR (Arch User Repository).
|
||||
Packages for Arch Linux are available via pacman and AUR (Arch User Repository).
|
||||
|
||||
There are two packages. The stable one which is built with the latest release
|
||||
and the git version which builds from the most recent commit.
|
||||
|
||||
- Stable: <https://aur.archlinux.org/packages/lazygit/>
|
||||
- Stable: `sudo pacman -S lazygit`
|
||||
- Development: <https://aur.archlinux.org/packages/lazygit-git/>
|
||||
|
||||
Instruction of how to install AUR content can be found here:
|
||||
@@ -166,6 +167,14 @@ may need to add `~/go/bin` to your \$PATH (MacOS/Linux), or `%HOME%\go\bin`
|
||||
(Windows). Not to be mistaked for `C:\Go\bin` (which is for Go's own binaries,
|
||||
not apps like Lazygit).
|
||||
|
||||
### Chocolatey (Windows)
|
||||
|
||||
You can install `lazygit` using [Chocolatey](https://chocolatey.org/):
|
||||
|
||||
```sh
|
||||
choco install lazygit
|
||||
```
|
||||
|
||||
### Manual
|
||||
|
||||
You'll need to [install Go](https://golang.org/doc/install)
|
||||
@@ -270,12 +279,10 @@ Run `lazygit --debug` in one terminal tab and `lazygit --logs` in another to vie
|
||||
|
||||
If you would like to support the development of lazygit, consider [sponsoring me](https://github.com/sponsors/jesseduffield) (github is matching all donations dollar-for-dollar for 12 months)
|
||||
|
||||
## Work in progress
|
||||
## FAQ
|
||||
|
||||
This is still a work in progress so there's still bugs to iron out and as this
|
||||
is my first project in Go the code could no doubt use an increase in quality,
|
||||
but I'll be improving on it whenever I find the time. If you have any feedback
|
||||
feel free to [raise an issue](https://github.com/jesseduffield/lazygit/issues)/[submit a PR](https://github.com/jesseduffield/lazygit/pulls).
|
||||
### I'm struggling to see the selected line
|
||||
see [here](https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#struggling-to-see-selected-line)
|
||||
|
||||
## Social
|
||||
|
||||
|
||||
510
docs/Config.md
510
docs/Config.md
@@ -2,176 +2,192 @@
|
||||
|
||||
Default path for the config file:
|
||||
|
||||
* Linux: `~/.config/jesseduffield/lazygit/config.yml`
|
||||
* MacOS: `~/Library/Application Support/jesseduffield/lazygit/config.yml`
|
||||
* Windows: `%APPDATA%\jesseduffield\lazygit\config.yml`
|
||||
- Linux: `~/.config/lazygit/config.yml`
|
||||
- MacOS: `~/Library/Application Support/lazygit/config.yml`
|
||||
- Windows: `%APPDATA%\lazygit\config.yml`
|
||||
|
||||
For old installations (slightly embarrassing: I didn't realise at the time that you didn't need to supply a vendor name to the path so I just used my name):
|
||||
|
||||
- Linux: `~/.config/jesseduffield/lazygit/config.yml`
|
||||
- MacOS: `~/Library/Application Support/jesseduffield/lazygit/config.yml`
|
||||
- Windows: `%APPDATA%\jesseduffield\lazygit\config.yml`
|
||||
|
||||
## Default
|
||||
|
||||
```yaml
|
||||
gui:
|
||||
# stuff relating to the UI
|
||||
scrollHeight: 2 # how many lines you scroll by
|
||||
scrollPastBottom: true # enable scrolling past the bottom
|
||||
sidePanelWidth: 0.3333 # number from 0 to 1
|
||||
expandFocusedSidePanel: false
|
||||
mainPanelSplitMode: 'flexible' # one of 'horizontal' | 'flexible' | 'vertical'
|
||||
theme:
|
||||
lightTheme: false # For terminals with a light background
|
||||
activeBorderColor:
|
||||
- white
|
||||
- bold
|
||||
inactiveBorderColor:
|
||||
- green
|
||||
optionsTextColor:
|
||||
- blue
|
||||
selectedLineBgColor:
|
||||
- default
|
||||
selectedRangeBgColor:
|
||||
- blue
|
||||
commitLength:
|
||||
show: true
|
||||
mouseEvents: true
|
||||
skipUnstageLineWarning: false
|
||||
skipStashWarning: true
|
||||
git:
|
||||
paging:
|
||||
colorArg: always
|
||||
useConfig: false
|
||||
merging:
|
||||
# only applicable to unix users
|
||||
manualCommit: false
|
||||
# extra args passed to `git merge`, e.g. --no-ff
|
||||
args: ""
|
||||
pull:
|
||||
mode: 'merge' # one of 'merge' | 'rebase' | 'ff-only'
|
||||
skipHookPrefix: WIP
|
||||
autoFetch: true
|
||||
branchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --"
|
||||
overrideGpg: false # prevents lazygit from spawning a separate process when using GPG
|
||||
disableForcePushing: false
|
||||
update:
|
||||
method: prompt # can be: prompt | background | never
|
||||
days: 14 # how often an update is checked for
|
||||
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
|
||||
confirmOnQuit: false
|
||||
# determines whether hitting 'esc' will quit the application when there is nothing to cancel/close
|
||||
quitOnTopLevelReturn: true
|
||||
disableStartupPopups: false
|
||||
keybinding:
|
||||
universal:
|
||||
quit: 'q'
|
||||
quit-alt1: '<c-c>' # alternative/alias of quit
|
||||
return: '<esc>' # return to previous menu, will quit if there's nowhere to return
|
||||
quitWithoutChangingDirectory: 'Q'
|
||||
togglePanel: '<tab>' # goto the next panel
|
||||
prevItem: '<up>' # go one line up
|
||||
nextItem: '<down>' # go one line down
|
||||
prevItem-alt: 'k' # go one line up
|
||||
nextItem-alt: 'j' # go one line down
|
||||
prevPage: ',' # go to next page in list
|
||||
nextPage: '.' # go to previous page in list
|
||||
gotoTop: '<' # go to top of list
|
||||
gotoBottom: '>' # go to bottom of list
|
||||
prevBlock: '<left>' # goto the previous block / panel
|
||||
nextBlock: '<right>' # goto the next block / panel
|
||||
prevBlock-alt: 'h' # goto the previous block / panel
|
||||
nextBlock-alt: 'l' # goto the next block / panel
|
||||
nextMatch: 'n'
|
||||
prevMatch: 'N'
|
||||
optionMenu: 'x' # show help menu
|
||||
optionMenu-alt1: '?' # show help menu
|
||||
select: '<space>'
|
||||
goInto: '<enter>'
|
||||
confirm: '<enter>'
|
||||
confirm-alt1: 'y'
|
||||
remove: 'd'
|
||||
new: 'n'
|
||||
edit: 'e'
|
||||
openFile: 'o'
|
||||
scrollUpMain: '<pgup>' # main panel scrool up
|
||||
scrollDownMain: '<pgdown>' # main panel scrool down
|
||||
scrollUpMain-alt1: 'K' # main panel scrool up
|
||||
scrollDownMain-alt1: 'J' # main panel scrool down
|
||||
scrollUpMain-alt2: '<c-u>' # main panel scrool up
|
||||
scrollDownMain-alt2: '<c-d>' # main panel scrool down
|
||||
executeCustomCommand: ':'
|
||||
createRebaseOptionsMenu: 'm'
|
||||
pushFiles: 'P'
|
||||
pullFiles: 'p'
|
||||
refresh: 'R'
|
||||
createPatchOptionsMenu: '<c-p>'
|
||||
nextTab: ']'
|
||||
prevTab: '['
|
||||
nextScreenMode: '+'
|
||||
prevScreenMode: '_'
|
||||
undo: 'z'
|
||||
redo: '<c-z>'
|
||||
filteringMenu: '<c-s>'
|
||||
diffingMenu: 'W'
|
||||
diffingMenu-alt: '<c-e>' # deprecated
|
||||
copyToClipboard: '<c-o>'
|
||||
submitEditorText: '<enter>'
|
||||
appendNewline: '<tab>'
|
||||
status:
|
||||
checkForUpdate: 'u'
|
||||
recentRepos: '<enter>'
|
||||
files:
|
||||
commitChanges: 'c'
|
||||
commitChangesWithoutHook: 'w' # commit changes without pre-commit hook
|
||||
amendLastCommit: 'A'
|
||||
commitChangesWithEditor: 'C'
|
||||
ignoreFile: 'i'
|
||||
refreshFiles: 'r'
|
||||
stashAllChanges: 's'
|
||||
viewStashOptions: 'S'
|
||||
toggleStagedAll: 'a' # stage/unstage all
|
||||
viewResetOptions: 'D'
|
||||
fetch: 'f'
|
||||
branches:
|
||||
createPullRequest: 'o'
|
||||
checkoutBranchByName: 'c'
|
||||
forceCheckoutBranch: 'F'
|
||||
rebaseBranch: 'r'
|
||||
mergeIntoCurrentBranch: 'M'
|
||||
viewGitFlowOptions: 'i'
|
||||
fastForward: 'f' # fast-forward this branch from its upstream
|
||||
pushTag: 'P'
|
||||
setUpstream: 'u' # set as upstream of checked-out branch
|
||||
fetchRemote: 'f'
|
||||
commits:
|
||||
squashDown: 's'
|
||||
renameCommit: 'r'
|
||||
renameCommitWithEditor: 'R'
|
||||
viewResetOptions: 'g'
|
||||
markCommitAsFixup: 'f'
|
||||
createFixupCommit: 'F' # create fixup commit for this commit
|
||||
squashAboveCommits: 'S'
|
||||
moveDownCommit: '<c-j>' # move commit down one
|
||||
moveUpCommit: '<c-k>' # move commit up one
|
||||
amendToCommit: 'A'
|
||||
pickCommit: 'p' # pick commit (when mid-rebase)
|
||||
revertCommit: 't'
|
||||
cherryPickCopy: 'c'
|
||||
cherryPickCopyRange: 'C'
|
||||
pasteCommits: 'v'
|
||||
tagCommit: 'T'
|
||||
checkoutCommit: '<space>'
|
||||
resetCherryPick: '<c-R>'
|
||||
copyCommitMessageToClipboard: '<c-y>'
|
||||
stash:
|
||||
popStash: 'g'
|
||||
commitFiles:
|
||||
checkoutCommitFile: 'c'
|
||||
main:
|
||||
toggleDragSelect: 'v'
|
||||
toggleDragSelect-alt: 'V'
|
||||
toggleSelectHunk: 'a'
|
||||
pickBothHunks: 'b'
|
||||
submodules:
|
||||
init: 'i'
|
||||
update: 'u'
|
||||
bulkMenu: 'b'
|
||||
gui:
|
||||
# stuff relating to the UI
|
||||
scrollHeight: 2 # how many lines you scroll by
|
||||
scrollPastBottom: true # enable scrolling past the bottom
|
||||
sidePanelWidth: 0.3333 # number from 0 to 1
|
||||
expandFocusedSidePanel: false
|
||||
mainPanelSplitMode: 'flexible' # one of 'horizontal' | 'flexible' | 'vertical'
|
||||
theme:
|
||||
lightTheme: false # For terminals with a light background
|
||||
activeBorderColor:
|
||||
- white
|
||||
- bold
|
||||
inactiveBorderColor:
|
||||
- green
|
||||
optionsTextColor:
|
||||
- blue
|
||||
selectedLineBgColor:
|
||||
- default
|
||||
selectedRangeBgColor:
|
||||
- blue
|
||||
commitLength:
|
||||
show: true
|
||||
mouseEvents: true
|
||||
skipUnstageLineWarning: false
|
||||
skipStashWarning: true
|
||||
showFileTree: false # for rendering changes files in a tree format
|
||||
showRandomTip: true
|
||||
showCommandLog: true
|
||||
commandLogSize: 8
|
||||
git:
|
||||
paging:
|
||||
colorArg: always
|
||||
useConfig: false
|
||||
merging:
|
||||
# only applicable to unix users
|
||||
manualCommit: false
|
||||
# extra args passed to `git merge`, e.g. --no-ff
|
||||
args: ''
|
||||
pull:
|
||||
mode: 'merge' # one of 'merge' | 'rebase' | 'ff-only'
|
||||
skipHookPrefix: WIP
|
||||
autoFetch: true
|
||||
branchLogCmd: 'git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --'
|
||||
allBranchesLogCmd: 'git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium'
|
||||
overrideGpg: false # prevents lazygit from spawning a separate process when using GPG
|
||||
disableForcePushing: false
|
||||
refresher:
|
||||
refreshInterval: 10 # file/submodule refresh interval in seconds
|
||||
fetchInterval: 60 # re-fetch interval in seconds
|
||||
update:
|
||||
method: prompt # can be: prompt | background | never
|
||||
days: 14 # how often an update is checked for
|
||||
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
|
||||
confirmOnQuit: false
|
||||
# determines whether hitting 'esc' will quit the application when there is nothing to cancel/close
|
||||
quitOnTopLevelReturn: false
|
||||
disableStartupPopups: false
|
||||
notARepository: 'prompt' # one of: 'prompt' | 'create' | 'skip'
|
||||
keybinding:
|
||||
universal:
|
||||
quit: 'q'
|
||||
quit-alt1: '<c-c>' # alternative/alias of quit
|
||||
return: '<esc>' # return to previous menu, will quit if there's nowhere to return
|
||||
quitWithoutChangingDirectory: 'Q'
|
||||
togglePanel: '<tab>' # goto the next panel
|
||||
prevItem: '<up>' # go one line up
|
||||
nextItem: '<down>' # go one line down
|
||||
prevItem-alt: 'k' # go one line up
|
||||
nextItem-alt: 'j' # go one line down
|
||||
prevPage: ',' # go to next page in list
|
||||
nextPage: '.' # go to previous page in list
|
||||
gotoTop: '<' # go to top of list
|
||||
gotoBottom: '>' # go to bottom of list
|
||||
prevBlock: '<left>' # goto the previous block / panel
|
||||
nextBlock: '<right>' # goto the next block / panel
|
||||
prevBlock-alt: 'h' # goto the previous block / panel
|
||||
nextBlock-alt: 'l' # goto the next block / panel
|
||||
nextMatch: 'n'
|
||||
prevMatch: 'N'
|
||||
optionMenu: 'x' # show help menu
|
||||
optionMenu-alt1: '?' # show help menu
|
||||
select: '<space>'
|
||||
goInto: '<enter>'
|
||||
confirm: '<enter>'
|
||||
confirm-alt1: 'y'
|
||||
remove: 'd'
|
||||
new: 'n'
|
||||
edit: 'e'
|
||||
openFile: 'o'
|
||||
scrollUpMain: '<pgup>' # main panel scroll up
|
||||
scrollDownMain: '<pgdown>' # main panel scroll down
|
||||
scrollUpMain-alt1: 'K' # main panel scroll up
|
||||
scrollDownMain-alt1: 'J' # main panel scroll down
|
||||
scrollUpMain-alt2: '<c-u>' # main panel scroll up
|
||||
scrollDownMain-alt2: '<c-d>' # main panel scroll down
|
||||
executeCustomCommand: ':'
|
||||
createRebaseOptionsMenu: 'm'
|
||||
pushFiles: 'P'
|
||||
pullFiles: 'p'
|
||||
refresh: 'R'
|
||||
createPatchOptionsMenu: '<c-p>'
|
||||
nextTab: ']'
|
||||
prevTab: '['
|
||||
nextScreenMode: '+'
|
||||
prevScreenMode: '_'
|
||||
undo: 'z'
|
||||
redo: '<c-z>'
|
||||
filteringMenu: '<c-s>'
|
||||
diffingMenu: 'W'
|
||||
diffingMenu-alt: '<c-e>' # deprecated
|
||||
copyToClipboard: '<c-o>'
|
||||
submitEditorText: '<enter>'
|
||||
appendNewline: '<tab>'
|
||||
status:
|
||||
checkForUpdate: 'u'
|
||||
recentRepos: '<enter>'
|
||||
files:
|
||||
commitChanges: 'c'
|
||||
commitChangesWithoutHook: 'w' # commit changes without pre-commit hook
|
||||
amendLastCommit: 'A'
|
||||
commitChangesWithEditor: 'C'
|
||||
ignoreFile: 'i'
|
||||
refreshFiles: 'r'
|
||||
stashAllChanges: 's'
|
||||
viewStashOptions: 'S'
|
||||
toggleStagedAll: 'a' # stage/unstage all
|
||||
viewResetOptions: 'D'
|
||||
fetch: 'f'
|
||||
toggleTreeView: '`'
|
||||
branches:
|
||||
createPullRequest: 'o'
|
||||
checkoutBranchByName: 'c'
|
||||
forceCheckoutBranch: 'F'
|
||||
rebaseBranch: 'r'
|
||||
mergeIntoCurrentBranch: 'M'
|
||||
viewGitFlowOptions: 'i'
|
||||
fastForward: 'f' # fast-forward this branch from its upstream
|
||||
pushTag: 'P'
|
||||
setUpstream: 'u' # set as upstream of checked-out branch
|
||||
fetchRemote: 'f'
|
||||
commits:
|
||||
squashDown: 's'
|
||||
renameCommit: 'r'
|
||||
renameCommitWithEditor: 'R'
|
||||
viewResetOptions: 'g'
|
||||
markCommitAsFixup: 'f'
|
||||
createFixupCommit: 'F' # create fixup commit for this commit
|
||||
squashAboveCommits: 'S'
|
||||
moveDownCommit: '<c-j>' # move commit down one
|
||||
moveUpCommit: '<c-k>' # move commit up one
|
||||
amendToCommit: 'A'
|
||||
pickCommit: 'p' # pick commit (when mid-rebase)
|
||||
revertCommit: 't'
|
||||
cherryPickCopy: 'c'
|
||||
cherryPickCopyRange: 'C'
|
||||
pasteCommits: 'v'
|
||||
tagCommit: 'T'
|
||||
checkoutCommit: '<space>'
|
||||
resetCherryPick: '<c-R>'
|
||||
copyCommitMessageToClipboard: '<c-y>'
|
||||
stash:
|
||||
popStash: 'g'
|
||||
commitFiles:
|
||||
checkoutCommitFile: 'c'
|
||||
main:
|
||||
toggleDragSelect: 'v'
|
||||
toggleDragSelect-alt: 'V'
|
||||
toggleSelectHunk: 'a'
|
||||
pickBothHunks: 'b'
|
||||
submodules:
|
||||
init: 'i'
|
||||
update: 'u'
|
||||
bulkMenu: 'b'
|
||||
```
|
||||
|
||||
## Platform Defaults
|
||||
@@ -179,22 +195,22 @@ Default path for the config file:
|
||||
### Windows
|
||||
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'cmd /c "start "" {{filename}}"'
|
||||
os:
|
||||
openCommand: 'cmd /c "start "" {{filename}}"'
|
||||
```
|
||||
|
||||
### Linux
|
||||
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'
|
||||
os:
|
||||
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'
|
||||
```
|
||||
|
||||
### OSX
|
||||
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'open {{filename}}'
|
||||
os:
|
||||
openCommand: 'open {{filename}}'
|
||||
```
|
||||
|
||||
### Recommended Config Values
|
||||
@@ -202,8 +218,8 @@ Default path for the config file:
|
||||
for users of VSCode
|
||||
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'code -rg {{filename}}'
|
||||
os:
|
||||
openCommand: 'code -rg {{filename}}'
|
||||
```
|
||||
|
||||
## Color Attributes
|
||||
@@ -229,31 +245,50 @@ The available attributes are:
|
||||
If you have issues with a light terminal theme where you can't read / see the text add these settings
|
||||
|
||||
```yaml
|
||||
gui:
|
||||
theme:
|
||||
lightTheme: true
|
||||
activeBorderColor:
|
||||
- black
|
||||
- bold
|
||||
inactiveBorderColor:
|
||||
- black
|
||||
selectedLineBgColor:
|
||||
- default
|
||||
gui:
|
||||
theme:
|
||||
lightTheme: true
|
||||
activeBorderColor:
|
||||
- black
|
||||
- bold
|
||||
inactiveBorderColor:
|
||||
- black
|
||||
selectedLineBgColor:
|
||||
- default
|
||||
```
|
||||
|
||||
## Struggling to see selected line
|
||||
|
||||
If you struggle to see the selected line I recomment using the reverse attribute on selected lines like so:
|
||||
If you struggle to see the selected line I recommend using the reverse attribute on selected lines like so:
|
||||
|
||||
```yaml
|
||||
gui:
|
||||
theme:
|
||||
selectedLineBgColor:
|
||||
- reverse
|
||||
selectedRangeBgColor:
|
||||
- reverse
|
||||
gui:
|
||||
theme:
|
||||
selectedLineBgColor:
|
||||
- reverse
|
||||
selectedRangeBgColor:
|
||||
- reverse
|
||||
```
|
||||
|
||||
The following has also worked for a couple of people:
|
||||
|
||||
```yaml
|
||||
gui:
|
||||
theme:
|
||||
activeBorderColor:
|
||||
- white
|
||||
- bold
|
||||
inactiveBorderColor:
|
||||
- white
|
||||
selectedLineBgColor:
|
||||
- reverse
|
||||
- blue
|
||||
```
|
||||
|
||||
Alternatively you may have bold fonts disabled in your terminal, in which case enabling bold fonts should solve the problem.
|
||||
|
||||
If you're still having trouble please raise an issue.
|
||||
|
||||
## Example Coloring
|
||||
|
||||

|
||||
@@ -265,33 +300,33 @@ For all possible keybinding options, check [Custom_Keybindings.md](https://githu
|
||||
### Example Keybindings For Colemak Users
|
||||
|
||||
```yaml
|
||||
keybinding:
|
||||
universal:
|
||||
prevItem-alt: 'u'
|
||||
nextItem-alt: 'e'
|
||||
prevBlock-alt: 'n'
|
||||
nextBlock-alt: 'i'
|
||||
nextMatch: '='
|
||||
prevMatch: '-'
|
||||
new: 'k'
|
||||
edit: 'o'
|
||||
openFile: 'O'
|
||||
scrollUpMain-alt1: 'U'
|
||||
scrollDownMain-alt1: 'E'
|
||||
scrollUpMain-alt2: '<c-u>'
|
||||
scrollDownMain-alt2: '<c-e>'
|
||||
undo: 'l'
|
||||
redo: '<c-r>'
|
||||
diffingMenu: 'M'
|
||||
filteringMenu: '<c-f>'
|
||||
files:
|
||||
ignoreFile: 'I'
|
||||
commits:
|
||||
moveDownCommit: '<c-e>'
|
||||
moveUpCommit: '<c-u>'
|
||||
branches:
|
||||
viewGitFlowOptions: 'I'
|
||||
setUpstream: 'U'
|
||||
keybinding:
|
||||
universal:
|
||||
prevItem-alt: 'u'
|
||||
nextItem-alt: 'e'
|
||||
prevBlock-alt: 'n'
|
||||
nextBlock-alt: 'i'
|
||||
nextMatch: '='
|
||||
prevMatch: '-'
|
||||
new: 'k'
|
||||
edit: 'o'
|
||||
openFile: 'O'
|
||||
scrollUpMain-alt1: 'U'
|
||||
scrollDownMain-alt1: 'E'
|
||||
scrollUpMain-alt2: '<c-u>'
|
||||
scrollDownMain-alt2: '<c-e>'
|
||||
undo: 'l'
|
||||
redo: '<c-r>'
|
||||
diffingMenu: 'M'
|
||||
filteringMenu: '<c-f>'
|
||||
files:
|
||||
ignoreFile: 'I'
|
||||
commits:
|
||||
moveDownCommit: '<c-e>'
|
||||
moveUpCommit: '<c-u>'
|
||||
branches:
|
||||
viewGitFlowOptions: 'I'
|
||||
setUpstream: 'U'
|
||||
```
|
||||
|
||||
## Custom pull request URLs
|
||||
@@ -302,7 +337,7 @@ the pull request. You can do so on your `config.yml` file using the following sy
|
||||
|
||||
```yaml
|
||||
services:
|
||||
"<gitDomain>": "<provider>:<webDomain>"
|
||||
'<gitDomain>': '<provider>:<webDomain>'
|
||||
```
|
||||
|
||||
Where:
|
||||
@@ -312,19 +347,21 @@ Where:
|
||||
- `webDomain` is the URL where your git service exposes a web interface and APIs, e.g. `gitservice.work.com`
|
||||
|
||||
## Predefined commit message prefix
|
||||
|
||||
In situations where certain naming pattern is used for branches and commits, pattern can be used to populate
|
||||
commit message with prefix that is parsed from the branch name.
|
||||
|
||||
Example:
|
||||
* Branch name: feature/AB-123
|
||||
* Commit message: [AB-123] Adding feature
|
||||
|
||||
- Branch name: feature/AB-123
|
||||
- Commit message: [AB-123] Adding feature
|
||||
|
||||
```yaml
|
||||
git:
|
||||
commitPrefixes:
|
||||
my_project: # This is repository folder name
|
||||
pattern: "^\\w+\\/(\\w+-\\w+)"
|
||||
replace: "[$1] "
|
||||
git:
|
||||
commitPrefixes:
|
||||
my_project: # This is repository folder name
|
||||
pattern: "^\\w+\\/(\\w+-\\w+).*"
|
||||
replace: '[$1] '
|
||||
```
|
||||
|
||||
## Custom git log command
|
||||
@@ -339,3 +376,24 @@ git:
|
||||
Result:
|
||||
|
||||

|
||||
|
||||
## Launching not in a repository behaviour
|
||||
|
||||
By default, when launching lazygit from a directory that is not a repository,
|
||||
you will be prompted to choose if you would like to initialize a repo. You can
|
||||
override this behaviour in the config with one of the following:
|
||||
|
||||
```yaml
|
||||
# for default prompting behaviour
|
||||
notARepository: 'prompt'
|
||||
```
|
||||
|
||||
```yaml
|
||||
# to skip and initialize a new repo
|
||||
notARepository: 'create'
|
||||
```
|
||||
|
||||
```yaml
|
||||
# to skip without creating a new repo
|
||||
notARepository: 'skip'
|
||||
```
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
You can add custom command keybindings in your config.yml (accessible by pressing 'o' on the status panel from within lazygit) like so:
|
||||
|
||||
```
|
||||
```yml
|
||||
customCommands:
|
||||
- key: '<c-r>'
|
||||
command: 'hub browse -- "commit/{{.SelectedLocalCommit.Sha}}"'
|
||||
@@ -95,7 +95,7 @@ The permitted option fields are:
|
||||
|
||||
If an option has no name the value will be displayed to the user in place of the name, so you're allowed to only include the value like so:
|
||||
|
||||
```
|
||||
```yml
|
||||
prompts:
|
||||
- type: 'menu'
|
||||
title: 'What kind of branch is it?'
|
||||
@@ -123,7 +123,7 @@ SelectedCommitFile
|
||||
CheckedOutBranch
|
||||
```
|
||||
|
||||
To see what fields are available on e.g. the `SelectedFile`, see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/commands/file.go) (all the modelling lives in the same directory). Note that the custom commands feature does not guarantee backwards compatibility (until we hit lazygit version 1.0 of course) which means a field you're accessing on an object may no longer be available from one release to the next. Typically however, all you'll need is `{{.SelectedFile.Name}}`, `{{.SelectedLocalCommit.Sha}}` and `{{.SelectedBranch.Name}}`. In the future we will likely introduce a tighter interface that exposes a limited set of fields for each model.
|
||||
To see what fields are available on e.g. the `SelectedFile`, see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/commands/models/file.go) (all the modelling lives in the same directory). Note that the custom commands feature does not guarantee backwards compatibility (until we hit lazygit version 1.0 of course) which means a field you're accessing on an object may no longer be available from one release to the next. Typically however, all you'll need is `{{.SelectedFile.Name}}`, `{{.SelectedLocalCommit.Sha}}` and `{{.SelectedBranch.Name}}`. In the future we will likely introduce a tighter interface that exposes a limited set of fields for each model.
|
||||
|
||||
### Keybinding collisions
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ the `colorArg` key is for whether you want the `--color=always` arg in your `git
|
||||
git:
|
||||
paging:
|
||||
colorArg: always
|
||||
pager: delta --dark --paging=never --24-bit-color=never
|
||||
pager: delta --dark --paging=never
|
||||
```
|
||||
|
||||

|
||||
|
||||
7
docs/README.md
Normal file
7
docs/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Documentation Overview
|
||||
|
||||
* [Configuration](./Config.md).
|
||||
* [Custom Commands](./Custom_Command_Keybindings.md)
|
||||
* [Custom Pagers](./Custom_Pagers.md)
|
||||
* [Keybindings](./keybindings)
|
||||
* [Undo/Redo](./Undoing.md)
|
||||
@@ -16,7 +16,7 @@
|
||||
<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>: view filter-by-path options
|
||||
<kbd>W</kbd>: open diff menu
|
||||
<kbd>ctrl+e</kbd>: open diff menu
|
||||
</pre>
|
||||
@@ -24,13 +24,13 @@
|
||||
## List Panel Navigation
|
||||
|
||||
<pre>
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>]</kbd>: next tab
|
||||
<kbd>[</kbd>: previous tab
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Branches Panel (Branches Tab)
|
||||
@@ -38,6 +38,7 @@
|
||||
<pre>
|
||||
<kbd>space</kbd>: checkout
|
||||
<kbd>o</kbd>: create pull request
|
||||
<kbd>ctrl+y</kbd>: copy pull request URL to clipboard
|
||||
<kbd>c</kbd>: checkout by name
|
||||
<kbd>F</kbd>: force checkout
|
||||
<kbd>n</kbd>: new branch
|
||||
@@ -108,7 +109,8 @@
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>e</kbd>: edit file
|
||||
<kbd>space</kbd>: toggle file included in patch
|
||||
<kbd>enter</kbd>: enter file to add selected lines to the patch
|
||||
<kbd>enter</kbd>: enter file to add selected lines to the patch (or toggle directory collapsed)
|
||||
<kbd>`</kbd>: toggle file tree view
|
||||
</pre>
|
||||
|
||||
## Commits Panel (Commits)
|
||||
@@ -137,6 +139,7 @@
|
||||
<kbd>n</kbd>: create new branch off of commit
|
||||
<kbd>T</kbd>: tag commit
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>ctrl+y</kbd>: copy commit message to clipboard
|
||||
</pre>
|
||||
|
||||
## Commits Panel (Reflog Tab)
|
||||
@@ -168,10 +171,11 @@
|
||||
<kbd>S</kbd>: view stash options
|
||||
<kbd>a</kbd>: stage/unstage all
|
||||
<kbd>D</kbd>: view reset options
|
||||
<kbd>enter</kbd>: stage individual hunks/lines
|
||||
<kbd>enter</kbd>: stage individual hunks/lines for file, or collapse/expand for directory
|
||||
<kbd>f</kbd>: fetch
|
||||
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
|
||||
<kbd>g</kbd>: view upstream reset options
|
||||
<kbd>`</kbd>: toggle file tree view
|
||||
</pre>
|
||||
|
||||
## Files Panel (Submodules)
|
||||
@@ -203,8 +207,8 @@
|
||||
## Main Panel (Normal)
|
||||
|
||||
<pre>
|
||||
<kbd> ̄</kbd>: scroll down (fn+up)
|
||||
<kbd>¦</kbd>: scroll up (fn+down)
|
||||
<kbd>Ő</kbd>: scroll down (fn+up)
|
||||
<kbd>ő</kbd>: scroll up (fn+down)
|
||||
</pre>
|
||||
|
||||
## Main Panel (Patch Building)
|
||||
@@ -267,4 +271,5 @@
|
||||
<kbd>o</kbd>: open config file
|
||||
<kbd>u</kbd>: check for update
|
||||
<kbd>enter</kbd>: switch to a recent repo
|
||||
<kbd>a</kbd>: show all branch logs
|
||||
</pre>
|
||||
|
||||
@@ -24,13 +24,13 @@
|
||||
## List Panel Navigation
|
||||
|
||||
<pre>
|
||||
<kbd>.</kbd>: volgende pagina
|
||||
<kbd>,</kbd>: vorige pagina
|
||||
<kbd><</kbd>: scroll naar boven
|
||||
<kbd>></kbd>: scroll naar beneden
|
||||
<kbd>/</kbd>: start met zoekken
|
||||
<kbd>]</kbd>: volgende tab
|
||||
<kbd>[</kbd>: vorige tab
|
||||
<kbd>,</kbd>: vorige pagina
|
||||
<kbd>.</kbd>: volgende pagina
|
||||
<kbd><</kbd>: scroll naar boven
|
||||
<kbd>/</kbd>: start met zoekken
|
||||
<kbd>></kbd>: scroll naar beneden
|
||||
</pre>
|
||||
|
||||
## Branches Paneel (Branches Tab)
|
||||
@@ -38,6 +38,7 @@
|
||||
<pre>
|
||||
<kbd>space</kbd>: uitchecken
|
||||
<kbd>o</kbd>: maak een pull-aanvraag
|
||||
<kbd>ctrl+y</kbd>: kopieer de URL van het pull-verzoek naar het klembord
|
||||
<kbd>c</kbd>: uitchecken bij naam
|
||||
<kbd>F</kbd>: forceer checkout
|
||||
<kbd>n</kbd>: nieuwe branch
|
||||
@@ -109,6 +110,7 @@
|
||||
<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>: toggle file tree view
|
||||
</pre>
|
||||
|
||||
## Commits Paneel (Commits)
|
||||
@@ -137,6 +139,7 @@
|
||||
<kbd>n</kbd>: create new branch off of commit
|
||||
<kbd>T</kbd>: tag commit
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (gecopieerde) commits selectie
|
||||
<kbd>ctrl+y</kbd>: copieer commit bericht naar clipboard
|
||||
</pre>
|
||||
|
||||
## Commits Paneel (Reflog Tab)
|
||||
@@ -172,6 +175,7 @@
|
||||
<kbd>f</kbd>: fetch
|
||||
<kbd>ctrl+o</kbd>: kopieer de bestandsnaam naar het klembord
|
||||
<kbd>g</kbd>: bekijk upstream reset opties
|
||||
<kbd>`</kbd>: toggle file tree view
|
||||
</pre>
|
||||
|
||||
## Bestanden Paneel (Submodules)
|
||||
@@ -203,8 +207,8 @@
|
||||
## Hooft Paneel (Normaal)
|
||||
|
||||
<pre>
|
||||
<kbd> ̄</kbd>: scroll omlaag (fn+up)
|
||||
<kbd>¦</kbd>: scroll omhoog (fn+down)
|
||||
<kbd>Ő</kbd>: scroll omlaag (fn+up)
|
||||
<kbd>ő</kbd>: scroll omhoog (fn+down)
|
||||
</pre>
|
||||
|
||||
## Hooft Paneel (Patch Bouwen)
|
||||
@@ -267,4 +271,5 @@
|
||||
<kbd>o</kbd>: open config bestand
|
||||
<kbd>u</kbd>: check voor updates
|
||||
<kbd>enter</kbd>: wissel naar een recente repo
|
||||
<kbd>a</kbd>: alle takken van het houtblok laten zien
|
||||
</pre>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<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>: view filter-by-path options
|
||||
<kbd>W</kbd>: open diff menu
|
||||
<kbd>ctrl+e</kbd>: open diff menu
|
||||
</pre>
|
||||
@@ -24,13 +24,13 @@
|
||||
## List Panel Navigation
|
||||
|
||||
<pre>
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>]</kbd>: next tab
|
||||
<kbd>[</kbd>: previous tab
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Gałęzie Panel (Branches Tab)
|
||||
@@ -38,6 +38,7 @@
|
||||
<pre>
|
||||
<kbd>space</kbd>: przełącz
|
||||
<kbd>o</kbd>: utwórz żądanie wyciągnięcia
|
||||
<kbd>ctrl+y</kbd>: skopiuj adres URL żądania ściągnięcia do schowka
|
||||
<kbd>c</kbd>: przełącz używając nazwy
|
||||
<kbd>F</kbd>: wymuś przełączenie
|
||||
<kbd>n</kbd>: nowa gałąź
|
||||
@@ -108,7 +109,8 @@
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>e</kbd>: edytuj plik
|
||||
<kbd>space</kbd>: toggle file included in patch
|
||||
<kbd>enter</kbd>: enter file to add selected lines to the patch
|
||||
<kbd>enter</kbd>: enter file to add selected lines to the patch (or toggle directory collapsed)
|
||||
<kbd>`</kbd>: toggle file tree view
|
||||
</pre>
|
||||
|
||||
## Commity Panel (Commity)
|
||||
@@ -137,6 +139,7 @@
|
||||
<kbd>n</kbd>: create new branch off of commit
|
||||
<kbd>T</kbd>: tag commit
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>ctrl+y</kbd>: copy commit message to clipboard
|
||||
</pre>
|
||||
|
||||
## Commity Panel (Reflog Tab)
|
||||
@@ -172,6 +175,7 @@
|
||||
<kbd>f</kbd>: fetch
|
||||
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
|
||||
<kbd>g</kbd>: view upstream reset options
|
||||
<kbd>`</kbd>: toggle file tree view
|
||||
</pre>
|
||||
|
||||
## Pliki Panel (Submodules)
|
||||
@@ -203,8 +207,8 @@
|
||||
## Main Panel (Normal)
|
||||
|
||||
<pre>
|
||||
<kbd> ̄</kbd>: scroll down (fn+up)
|
||||
<kbd>¦</kbd>: scroll up (fn+down)
|
||||
<kbd>Ő</kbd>: scroll down (fn+up)
|
||||
<kbd>ő</kbd>: scroll up (fn+down)
|
||||
</pre>
|
||||
|
||||
## Main Panel (Patch Building)
|
||||
@@ -267,4 +271,5 @@
|
||||
<kbd>o</kbd>: otwórz plik konfiguracyjny
|
||||
<kbd>u</kbd>: sprawdź aktualizacje
|
||||
<kbd>enter</kbd>: switch to a recent repo
|
||||
<kbd>a</kbd>: pokazywać wszystkie logi branżowe
|
||||
</pre>
|
||||
|
||||
17
go.mod
17
go.mod
@@ -6,10 +6,12 @@ require (
|
||||
github.com/OpenPeeDeeP/xdg v1.0.0
|
||||
github.com/atotto/clipboard v0.1.2
|
||||
github.com/aybabtme/humanlog v0.4.1
|
||||
github.com/cli/safeexec v1.0.0
|
||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
|
||||
github.com/creack/pty v1.1.11
|
||||
github.com/fatih/color v1.9.0
|
||||
github.com/fsnotify/fsnotify v1.4.7
|
||||
github.com/gdamore/tcell/v2 v2.2.0 // indirect
|
||||
github.com/go-errors/errors v1.1.1
|
||||
github.com/go-logfmt/logfmt v0.5.0 // indirect
|
||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
|
||||
@@ -18,23 +20,28 @@ require (
|
||||
github.com/imdario/mergo v0.3.11
|
||||
github.com/integrii/flaggy v1.4.0
|
||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20201010224802-8a6768078fd7
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210417110745-37f79434200d
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe // indirect
|
||||
github.com/jesseduffield/yaml v2.1.0+incompatible
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.7 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.9
|
||||
github.com/mattn/go-runewidth v0.0.12
|
||||
github.com/mgutz/str v1.2.0
|
||||
github.com/onsi/ginkgo v1.10.3 // indirect
|
||||
github.com/onsi/gomega v1.7.1 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/sahilm/fuzzy v0.1.0
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
|
||||
github.com/stretchr/testify v1.4.0
|
||||
github.com/tcnksm/go-gitconfig v0.1.2
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 // indirect
|
||||
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c // indirect
|
||||
golang.org/x/sys v0.0.0-20201005172224-997123666555 // indirect
|
||||
golang.org/x/sys v0.0.0-20210415045647-66c3f260301c // indirect
|
||||
golang.org/x/term v0.0.0-20210406210042-72f3dc4e9b72 // indirect
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
)
|
||||
|
||||
replace github.com/go-git/go-git/v5 => github.com/jesseduffield/go-git/v5 v5.1.1
|
||||
|
||||
100
go.sum
100
go.sum
@@ -11,6 +11,8 @@ github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn
|
||||
github.com/aybabtme/humanlog v0.4.1 h1:D8d9um55rrthJsP8IGSHBcti9lTb/XknmDAX6Zy8tek=
|
||||
github.com/aybabtme/humanlog v0.4.1/go.mod h1:B0bnQX4FTSU3oftPMTTPvENCy8LqixLDvYJA9TUCAGo=
|
||||
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
|
||||
github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI=
|
||||
github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
|
||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do=
|
||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
@@ -29,8 +31,17 @@ github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjr
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell/v2 v2.0.0 h1:GRWG8aLfWAlekj9Q6W29bVvkHENc6hp79XOqG4AWDOs=
|
||||
github.com/gdamore/tcell/v2 v2.0.0/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
|
||||
github.com/gdamore/tcell/v2 v2.1.0 h1:UnSmozHgBkQi2PGsFr+rpdXuAPRRucMegpQp3Z3kDro=
|
||||
github.com/gdamore/tcell/v2 v2.1.0/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
|
||||
github.com/gdamore/tcell/v2 v2.2.0 h1:vSyEgKwraXPSOkvCk7IwOSyX+Pv3V2cV9CikJMXg4U4=
|
||||
github.com/gdamore/tcell/v2 v2.2.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
|
||||
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
|
||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
github.com/go-errors/errors v1.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=
|
||||
@@ -63,8 +74,52 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4 h1:GOQrmaE8i+KEdB8NzAegKYd4tPn/inM0I1uo0NXFerg=
|
||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20161105104656-d666c9f652af h1:9ZI/QyVOerYYeqMt4svycU2Lz0WvxNHCpHHbsFsi/oA=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20161105104656-d666c9f652af/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20201010224802-8a6768078fd7 h1:K3MGrjmpPtIhfXmKh/zsIF0CdmNKOkjpIwcUfAa/J2A=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20201010224802-8a6768078fd7/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20201224041937-f5a9733d1860 h1:1xfQM6T5A4jqcVvUnYaKR6bGrOhDLWQsp79JFNJpzcQ=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20201224041937-f5a9733d1860/go.mod h1:9LmtJcK+Kwiuc2huslzS37uFJPdHka2Cs/cQ06JZdbk=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210208224444-2eecee85583d h1:Jto9W9w8CFwZiAYXa7LsHDEOb5cKCA1f5LOL1A3jva4=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210208224444-2eecee85583d/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329125022-96b1d3106429 h1:Ih3UVczKRabZnQ7RisGi5uItC2QJxdqgef7AClJ2G9A=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329125022-96b1d3106429/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329125502-e830abf4b73a h1:RVYf2MA/RJbodE+S0e2z++JmB9A7hD1lUsI0euv1fmA=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329125502-e830abf4b73a/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329130738-e026850021e3 h1:UDiArPlzkg+8mmNjhUOamQoyiTSzQUGIpOsu5hCRJVI=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329130738-e026850021e3/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329131148-bcc4dcd991ff h1:fTt3EzLtpsc7OA7A6Vd6JVnlxvcAy7cY9lmN9yzDwSs=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329131148-bcc4dcd991ff/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210402033412-1238f910f001 h1:1WH+lTSK5YMr8emISHPA+VqYDDcLei6djuSxBCLIaiI=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210402033412-1238f910f001/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210402040718-77a1b9631715 h1:nELTdFJiZk3vv7j8nWoHvl7H2IqTr26EHKl6LaorRA8=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210402040718-77a1b9631715/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210402113210-6fd7ef27ce76 h1:miXVlortFNTlOX+KiKW3cVxOR6+Uhl4pnQRei2X26Y4=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210402113210-6fd7ef27ce76/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210403045716-a3be78c4ccf6 h1:nENhj0TKu+11RrPm9Ls5YtzkpbNHM0faXr9UECDhODQ=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210403045716-a3be78c4ccf6/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210405041826-439abd8b6e07 h1:BymGR28auSeuW0QELl0JomK0iFLPS/WRjFlc1iGZiOQ=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210405041826-439abd8b6e07/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210405093708-e79dab8f7772 h1:dg9krj10Udac4IcvlVCOAPktQkfggkgtqRmbDKk7Pzw=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210405093708-e79dab8f7772/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210406065811-95ef6e13779b h1:3+4+muhhikpls5FePXSRNFgcdoPx8dTdqaCy3AqLz98=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210406065811-95ef6e13779b/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210406065942-1b0c68414064 h1:Oe+QJuUIOd2TU+A3BW5sT1eXqceoBcOOfyoHlGf7F8Y=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210406065942-1b0c68414064/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210409121040-210802112d8a h1:ocrSuZxQIgWWt27b+rjiyIIPz6fzfFeoL5Q4cpa2cAo=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210409121040-210802112d8a/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210410011117-a2bb4baca390 h1:Es72JiUjt01TtvqCugdvOR91baB3DhuWF1DNuxA0frA=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210410011117-a2bb4baca390/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210412111008-6ef019af3724 h1:U70Do3/OSw5n/oLJGPWsQHnos2p0yq8yAeD2muioJhQ=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210412111008-6ef019af3724/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210412113212-ee65bd542c08 h1:d003y2GByfR3PqN/JvxNuqyo8vx4m0epwY2hW7sNU80=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210412113212-ee65bd542c08/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210412130453-de7bb5079f9f h1:JPpHlvSrKNxro+K9rM3nEHCdZ16qD0hnEedHPF07OtA=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210412130453-de7bb5079f9f/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210417105214-bdf37de5c917 h1:H4THGOdAJf61wByuq8EHF/NAgtqrTxpSIPsrCXU9HAY=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210417105214-bdf37de5c917/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210417110745-37f79434200d h1:2BPcc19W0j576hvhxtKma4jcD/+qAYvw1ln2HcIEZGU=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210417110745-37f79434200d/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe h1:qsVhCf2RFyyKIUe/+gJblbCpXMUki9rZrHuEctg6M/E=
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4=
|
||||
github.com/jesseduffield/yaml v2.1.0+incompatible h1:HWQJ1gIv2zHKbDYNp0Jwjlj24K8aqpFHnMCynY1EpmE=
|
||||
@@ -86,6 +141,12 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
@@ -97,8 +158,13 @@ github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGe
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-runewidth v0.0.7/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/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mgutz/str v1.2.0 h1:4IzWSdIz9qPQWLfKZ0rJcV0jcUDpxvP4JVZ4GXQyvSw=
|
||||
github.com/mgutz/str v1.2.0/go.mod h1:w1v0ofgLaJdoD0HpQ3fycxKD1WtxpjSo151pK/31q6w=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
@@ -114,6 +180,12 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
|
||||
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
@@ -126,8 +198,6 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
|
||||
github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE=
|
||||
github.com/urfave/cli v1.20.1-0.20180226030253-8e01ec4cd3e2/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
|
||||
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
|
||||
@@ -152,17 +222,39 @@ golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201005172224-997123666555 h1:fihtqzYxy4E31W1yUlyRGveTZT1JIP0bmKaDZ2ceKAw=
|
||||
golang.org/x/sys v0.0.0-20201005172224-997123666555/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210217105451-b926d437f341 h1:2/QtM1mL37YmcsT8HaDNHDgTqqFVw+zr8UzMiBVLzYU=
|
||||
golang.org/x/sys v0.0.0-20210217105451-b926d437f341/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54 h1:rF3Ohx8DRyl8h2zw9qojyLHLhrJpEMgyPOImREEryf0=
|
||||
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210402192133-700132347e07 h1:4k6HsQjxj6hVMsI2Vf0yKlzt5lXxZsMW1q0zaq2k8zY=
|
||||
golang.org/x/sys v0.0.0-20210402192133-700132347e07/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210415045647-66c3f260301c h1:6L+uOeS3OQt/f4eFHXZcTxeZrGCuz+CLElgEBjbcTA4=
|
||||
golang.org/x/sys v0.0.0-20210415045647-66c3f260301c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 h1:EC6+IGYTjPpRfv9a2b/6Puw0W+hLtAhkV1tPsXhutqs=
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210406210042-72f3dc4e9b72 h1:VqE9gduFZ4dbR7XoL77lHFp0/DyDUBKSXK7CMFkVcV0=
|
||||
golang.org/x/term v0.0.0-20210406210042-72f3dc4e9b72/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
|
||||
3
main.go
3
main.go
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/integrii/flaggy"
|
||||
"github.com/jesseduffield/lazygit/pkg/app"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/constants"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
yaml "github.com/jesseduffield/yaml"
|
||||
)
|
||||
@@ -134,6 +135,6 @@ func main() {
|
||||
stackTrace := newErr.ErrorStack()
|
||||
app.Log.Error(stackTrace)
|
||||
|
||||
log.Fatal(fmt.Sprintf("%s\n\n%s", app.Tr.ErrorOccurred, stackTrace))
|
||||
log.Fatal(fmt.Sprintf("%s: %s\n\n%s", app.Tr.ErrorOccurred, constants.Links.Issues, stackTrace))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
@@ -21,6 +20,7 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/updates"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -97,6 +97,7 @@ func newLogger(config config.AppConfigurer) *logrus.Entry {
|
||||
|
||||
// NewApp bootstrap a new application
|
||||
func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
|
||||
|
||||
app := &App{
|
||||
closers: []io.Closer{},
|
||||
Config: config,
|
||||
@@ -182,7 +183,7 @@ 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 err := commands.VerifyInGitRepo(app.OSCommand); err != nil {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -192,10 +193,20 @@ func (app *App) setupRepo() (bool, error) {
|
||||
return false, err // Current directory appears to be a git repository.
|
||||
}
|
||||
|
||||
// Offer to initialize a new repository in current directory.
|
||||
fmt.Print(app.Tr.CreateRepo)
|
||||
response, _ := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||
if strings.Trim(response, " \n") != "y" {
|
||||
shouldInitRepo := true
|
||||
notARepository := app.Config.GetUserConfig().NotARepository
|
||||
if notARepository == "prompt" {
|
||||
// Offer to initialize a new repository in current directory.
|
||||
fmt.Print(app.Tr.CreateRepo)
|
||||
response, _ := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||
if strings.Trim(response, " \n") != "y" {
|
||||
shouldInitRepo = false
|
||||
}
|
||||
} else if notARepository == "skip" {
|
||||
shouldInitRepo = false
|
||||
}
|
||||
|
||||
if !shouldInitRepo {
|
||||
// check if we have a recent repo we can open
|
||||
recentRepos := app.Config.GetAppState().RecentRepos
|
||||
if len(recentRepos) > 0 {
|
||||
@@ -215,6 +226,7 @@ func (app *App) setupRepo() (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -227,7 +239,7 @@ func (app *App) Run() error {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
err := app.Gui.RunWithSubprocesses()
|
||||
err := app.Gui.RunAndHandleError()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -314,7 +326,7 @@ func TailLogs() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
cmd := exec.Command("tail", "-f", logFilePath)
|
||||
cmd := secureexec.Command("tail", "-f", logFilePath)
|
||||
|
||||
stdout, _ := cmd.StdoutPipe()
|
||||
if err := cmd.Start(); err != nil {
|
||||
|
||||
@@ -11,19 +11,19 @@ import (
|
||||
|
||||
// NewBranch create new branch
|
||||
func (c *GitCommand) NewBranch(name string, base string) error {
|
||||
return c.OSCommand.RunCommand("git checkout -b %s %s", name, base)
|
||||
return c.RunCommand("git checkout -b %s %s", name, base)
|
||||
}
|
||||
|
||||
// CurrentBranchName get the current branch name and displayname.
|
||||
// the first returned string is the name and the second is the displayname
|
||||
// e.g. name is 123asdf and displayname is '(HEAD detached at 123asdf)'
|
||||
func (c *GitCommand) CurrentBranchName() (string, string, error) {
|
||||
branchName, err := c.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD")
|
||||
branchName, err := c.RunCommandWithOutput("git symbolic-ref --short HEAD")
|
||||
if err == nil && branchName != "HEAD\n" {
|
||||
trimmedBranchName := strings.TrimSpace(branchName)
|
||||
return trimmedBranchName, trimmedBranchName, nil
|
||||
}
|
||||
output, err := c.OSCommand.RunCommandWithOutput("git branch --contains")
|
||||
output, err := c.RunCommandWithOutput("git branch --contains")
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
@@ -59,9 +59,9 @@ type CheckoutOptions struct {
|
||||
func (c *GitCommand) Checkout(branch string, options CheckoutOptions) error {
|
||||
forceArg := ""
|
||||
if options.Force {
|
||||
forceArg = "--force "
|
||||
forceArg = " --force"
|
||||
}
|
||||
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git checkout %s %s", forceArg, branch), oscommands.RunCommandOptions{EnvVars: options.EnvVars})
|
||||
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git checkout%s %s", forceArg, branch), oscommands.RunCommandOptions{EnvVars: options.EnvVars})
|
||||
}
|
||||
|
||||
// GetBranchGraph gets the color-formatted graph of the log for the given branch
|
||||
@@ -73,7 +73,7 @@ func (c *GitCommand) GetBranchGraph(branchName string) (string, error) {
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetUpstreamForBranch(branchName string) (string, error) {
|
||||
output, err := c.OSCommand.RunCommandWithOutput("git rev-parse --abbrev-ref --symbolic-full-name %s@{u}", branchName)
|
||||
output, err := c.RunCommandWithOutput("git rev-parse --abbrev-ref --symbolic-full-name %s@{u}", branchName)
|
||||
return strings.TrimSpace(output), err
|
||||
}
|
||||
|
||||
@@ -86,11 +86,11 @@ func (c *GitCommand) GetBranchGraphCmdStr(branchName string) string {
|
||||
}
|
||||
|
||||
func (c *GitCommand) SetUpstreamBranch(upstream string) error {
|
||||
return c.OSCommand.RunCommand("git branch -u %s", upstream)
|
||||
return c.RunCommand("git branch -u %s", upstream)
|
||||
}
|
||||
|
||||
func (c *GitCommand) SetBranchUpstream(remoteName string, remoteBranchName string, branchName string) error {
|
||||
return c.OSCommand.RunCommand("git branch --set-upstream-to=%s/%s %s", remoteName, remoteBranchName, branchName)
|
||||
return c.RunCommand("git branch --set-upstream-to=%s/%s %s", remoteName, remoteBranchName, branchName)
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetCurrentBranchUpstreamDifferenceCount() (string, string) {
|
||||
@@ -134,24 +134,28 @@ func (c *GitCommand) Merge(branchName string, opts MergeOpts) error {
|
||||
|
||||
// AbortMerge abort merge
|
||||
func (c *GitCommand) AbortMerge() error {
|
||||
return c.OSCommand.RunCommand("git merge --abort")
|
||||
return c.RunCommand("git merge --abort")
|
||||
}
|
||||
|
||||
func (c *GitCommand) IsHeadDetached() bool {
|
||||
err := c.OSCommand.RunCommand("git symbolic-ref -q HEAD")
|
||||
err := c.RunCommand("git symbolic-ref -q HEAD")
|
||||
return err != nil
|
||||
}
|
||||
|
||||
// ResetHardHead runs `git reset --hard`
|
||||
func (c *GitCommand) ResetHard(ref string) error {
|
||||
return c.OSCommand.RunCommand("git reset --hard " + ref)
|
||||
return c.RunCommand("git reset --hard " + ref)
|
||||
}
|
||||
|
||||
// ResetSoft runs `git reset --soft HEAD`
|
||||
func (c *GitCommand) ResetSoft(ref string) error {
|
||||
return c.OSCommand.RunCommand("git reset --soft " + ref)
|
||||
return c.RunCommand("git reset --soft " + ref)
|
||||
}
|
||||
|
||||
func (c *GitCommand) ResetMixed(ref string) error {
|
||||
return c.RunCommand("git reset --mixed " + ref)
|
||||
}
|
||||
|
||||
func (c *GitCommand) RenameBranch(oldName string, newName string) error {
|
||||
return c.OSCommand.RunCommand("git branch --move %s %s", oldName, newName)
|
||||
return c.RunCommand("git branch --move %s %s", oldName, newName)
|
||||
}
|
||||
|
||||
338
pkg/commands/branches_test.go
Normal file
338
pkg/commands/branches_test.go
Normal file
@@ -0,0 +1,338 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandGetCommitDifferences is a function.
|
||||
func TestGitCommandGetCommitDifferences(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(string, string)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Can't retrieve pushable count",
|
||||
func(string, ...string) *exec.Cmd {
|
||||
return secureexec.Command("test")
|
||||
},
|
||||
func(pushableCount string, pullableCount string) {
|
||||
assert.EqualValues(t, "?", pushableCount)
|
||||
assert.EqualValues(t, "?", pullableCount)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Can't retrieve pullable count",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
if args[1] == "HEAD..@{u}" {
|
||||
return secureexec.Command("test")
|
||||
}
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(pushableCount string, pullableCount string) {
|
||||
assert.EqualValues(t, "?", pushableCount)
|
||||
assert.EqualValues(t, "?", pullableCount)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Retrieve pullable and pushable count",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
if args[1] == "HEAD..@{u}" {
|
||||
return secureexec.Command("echo", "10")
|
||||
}
|
||||
|
||||
return secureexec.Command("echo", "11")
|
||||
},
|
||||
func(pushableCount string, pullableCount string) {
|
||||
assert.EqualValues(t, "11", pushableCount)
|
||||
assert.EqualValues(t, "10", pullableCount)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.GetCommitDifferences("HEAD", "@{u}"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandNewBranch is a function.
|
||||
func TestGitCommandNewBranch(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"checkout", "-b", "test", "master"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.NewBranch("test", "master"))
|
||||
}
|
||||
|
||||
// TestGitCommandDeleteBranch is a function.
|
||||
func TestGitCommandDeleteBranch(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
branch string
|
||||
force bool
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Delete a branch",
|
||||
"test",
|
||||
false,
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"branch", "-d", "test"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Force delete a branch",
|
||||
"test",
|
||||
true,
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"branch", "-D", "test"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
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.DeleteBranch(s.branch, s.force))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandMerge is a function.
|
||||
func TestGitCommandMerge(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"merge", "--no-edit", "test"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.Merge("test", MergeOpts{}))
|
||||
}
|
||||
|
||||
// TestGitCommandCheckout is a function.
|
||||
func TestGitCommandCheckout(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
force bool
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Checkout",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"checkout", "test"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Checkout forced",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"checkout", "--force", "test"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.Checkout("test", CheckoutOptions{Force: s.force}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandGetBranchGraph is a function.
|
||||
func TestGitCommandGetBranchGraph(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
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 secureexec.Command("echo")
|
||||
}
|
||||
_, err := gitCmd.GetBranchGraph("test")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGitCommandGetAllBranchGraph(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"log", "--graph", "--all", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium"}, args)
|
||||
return secureexec.Command("echo")
|
||||
}
|
||||
cmdStr := gitCmd.Config.GetUserConfig().Git.AllBranchesLogCmd
|
||||
_, err := gitCmd.OSCommand.RunCommandWithOutput(cmdStr)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// TestGitCommandCurrentBranchName is a function.
|
||||
func TestGitCommandCurrentBranchName(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(string, string, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"says we are on the master branch if we are",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.Equal(t, "git", cmd)
|
||||
return secureexec.Command("echo", "master")
|
||||
},
|
||||
func(name string, displayname string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "master", name)
|
||||
assert.EqualValues(t, "master", displayname)
|
||||
},
|
||||
},
|
||||
{
|
||||
"falls back to git `git branch --contains` if symbolic-ref fails",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
|
||||
switch args[0] {
|
||||
case "symbolic-ref":
|
||||
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
|
||||
return secureexec.Command("test")
|
||||
case "branch":
|
||||
assert.EqualValues(t, []string{"branch", "--contains"}, args)
|
||||
return secureexec.Command("echo", "* master")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
func(name string, displayname string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "master", name)
|
||||
assert.EqualValues(t, "master", displayname)
|
||||
},
|
||||
},
|
||||
{
|
||||
"handles a detached head",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
|
||||
switch args[0] {
|
||||
case "symbolic-ref":
|
||||
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
|
||||
return secureexec.Command("test")
|
||||
case "branch":
|
||||
assert.EqualValues(t, []string{"branch", "--contains"}, args)
|
||||
return secureexec.Command("echo", "* (HEAD detached at 123abcd)")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
func(name string, displayname string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "123abcd", name)
|
||||
assert.EqualValues(t, "(HEAD detached at 123abcd)", displayname)
|
||||
},
|
||||
},
|
||||
{
|
||||
"bubbles up error if there is one",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.Equal(t, "git", cmd)
|
||||
return secureexec.Command("test")
|
||||
},
|
||||
func(name string, displayname string, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.EqualValues(t, "", name)
|
||||
assert.EqualValues(t, "", displayname)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.CurrentBranchName())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandResetHard is a function.
|
||||
func TestGitCommandResetHard(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
ref string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
"HEAD",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: `git reset --hard HEAD`,
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.ResetHard(s.ref))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,6 @@ package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
@@ -12,7 +10,7 @@ import (
|
||||
|
||||
// RenameCommit renames the topmost commit with the given name
|
||||
func (c *GitCommand) RenameCommit(name string) error {
|
||||
return c.OSCommand.RunCommand("git commit --allow-empty --amend -m %s", c.OSCommand.Quote(name))
|
||||
return c.RunCommand("git commit --allow-empty --amend --only -m %s", c.OSCommand.Quote(name))
|
||||
}
|
||||
|
||||
// ResetToCommit reset to commit
|
||||
@@ -20,20 +18,19 @@ func (c *GitCommand) ResetToCommit(sha string, strength string, options oscomman
|
||||
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git reset --%s %s", strength, sha), options)
|
||||
}
|
||||
|
||||
// Commit commits to git
|
||||
func (c *GitCommand) Commit(message string, flags string) (*exec.Cmd, error) {
|
||||
func (c *GitCommand) CommitCmdStr(message string, flags string) string {
|
||||
splitMessage := strings.Split(message, "\n")
|
||||
lineArgs := ""
|
||||
for _, line := range splitMessage {
|
||||
lineArgs += fmt.Sprintf(" -m %s", strconv.Quote(line))
|
||||
lineArgs += fmt.Sprintf(" -m %s", c.OSCommand.Quote(line))
|
||||
}
|
||||
|
||||
command := fmt.Sprintf("git commit %s%s", flags, lineArgs)
|
||||
if c.usingGpg() {
|
||||
return c.OSCommand.ShellCommandFromString(command), nil
|
||||
flagsStr := ""
|
||||
if flags != "" {
|
||||
flagsStr = fmt.Sprintf(" %s", flags)
|
||||
}
|
||||
|
||||
return nil, c.OSCommand.RunCommand(command)
|
||||
return fmt.Sprintf("git commit%s%s", flagsStr, lineArgs)
|
||||
}
|
||||
|
||||
// Get the subject of the HEAD commit
|
||||
@@ -51,18 +48,12 @@ func (c *GitCommand) GetCommitMessage(commitSha string) (string, error) {
|
||||
}
|
||||
|
||||
// AmendHead amends HEAD with whatever is staged in your working tree
|
||||
func (c *GitCommand) AmendHead() (*exec.Cmd, error) {
|
||||
command := "git commit --amend --no-edit --allow-empty"
|
||||
if c.usingGpg() {
|
||||
return c.OSCommand.ShellCommandFromString(command), nil
|
||||
}
|
||||
|
||||
return nil, c.OSCommand.RunCommand(command)
|
||||
func (c *GitCommand) AmendHead() error {
|
||||
return c.OSCommand.RunCommand(c.AmendHeadCmdStr())
|
||||
}
|
||||
|
||||
// PrepareCommitAmendSubProcess prepares a subprocess for `git commit --amend --allow-empty`
|
||||
func (c *GitCommand) PrepareCommitAmendSubProcess() *exec.Cmd {
|
||||
return c.OSCommand.PrepareSubProcess("git", "commit", "--amend", "--allow-empty")
|
||||
func (c *GitCommand) AmendHeadCmdStr() string {
|
||||
return "git commit --amend --no-edit --allow-empty"
|
||||
}
|
||||
|
||||
func (c *GitCommand) ShowCmdStr(sha string, filterPath string) string {
|
||||
@@ -75,7 +66,7 @@ func (c *GitCommand) ShowCmdStr(sha string, filterPath string) string {
|
||||
|
||||
// Revert reverts the selected commit by sha
|
||||
func (c *GitCommand) Revert(sha string) error {
|
||||
return c.OSCommand.RunCommand("git revert %s", sha)
|
||||
return c.RunCommand("git revert %s", sha)
|
||||
}
|
||||
|
||||
// CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD
|
||||
@@ -95,5 +86,5 @@ func (c *GitCommand) CherryPickCommits(commits []*models.Commit) error {
|
||||
|
||||
// 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)
|
||||
return c.RunCommand("git commit --fixup=%s", sha)
|
||||
}
|
||||
|
||||
111
pkg/commands/commits_test.go
Normal file
111
pkg/commands/commits_test.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandRenameCommit is a function.
|
||||
func TestGitCommandRenameCommit(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.RenameCommit("test"))
|
||||
}
|
||||
|
||||
// TestGitCommandResetToCommit is a function.
|
||||
func TestGitCommandResetToCommit(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"reset", "--hard", "78976bc"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.ResetToCommit("78976bc", "hard", oscommands.RunCommandOptions{}))
|
||||
}
|
||||
|
||||
// TestGitCommandCommitStr is a function.
|
||||
func TestGitCommandCommitStr(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
message string
|
||||
flags string
|
||||
expected string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Commit",
|
||||
message: "test",
|
||||
flags: "",
|
||||
expected: "git commit -m \"test\"",
|
||||
},
|
||||
{
|
||||
testName: "Commit with --no-verify flag",
|
||||
message: "test",
|
||||
flags: "--no-verify",
|
||||
expected: "git commit --no-verify -m \"test\"",
|
||||
},
|
||||
{
|
||||
testName: "Commit with multiline message",
|
||||
message: "line1\nline2",
|
||||
flags: "",
|
||||
expected: "git commit -m \"line1\" -m \"line2\"",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
cmdStr := gitCmd.CommitCmdStr(s.message, s.flags)
|
||||
assert.Equal(t, s.expected, cmdStr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandCreateFixupCommit is a function.
|
||||
func TestGitCommandCreateFixupCommit(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
sha string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
"12345",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: `git commit --fixup=12345`,
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.CreateFixupCommit(s.sha))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ func (c *GitCommand) ConfiguredPager() string {
|
||||
if os.Getenv("PAGER") != "" {
|
||||
return os.Getenv("PAGER")
|
||||
}
|
||||
output, err := c.OSCommand.RunCommandWithOutput("git config --get-all core.pager")
|
||||
output, err := c.RunCommandWithOutput("git config --get-all core.pager")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
@@ -43,10 +43,20 @@ func (c *GitCommand) colorArg() string {
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetConfigValue(key string) string {
|
||||
output, err := c.OSCommand.RunCommandWithOutput("git config --get %s", key)
|
||||
if err != nil {
|
||||
// looks like this returns an error if there is no matching value which we're okay with
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(output)
|
||||
output, _ := c.getGitConfigValue(key)
|
||||
return output
|
||||
}
|
||||
|
||||
// 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().Git.OverrideGpg
|
||||
if overrideGpg {
|
||||
return false
|
||||
}
|
||||
|
||||
gpgsign := c.GetConfigValue("commit.gpgsign")
|
||||
value := strings.ToLower(gpgsign)
|
||||
|
||||
return value == "true" || value == "1" || value == "yes" || value == "on"
|
||||
}
|
||||
|
||||
70
pkg/commands/config_test.go
Normal file
70
pkg/commands/config_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandUsingGpg is a function.
|
||||
func TestGitCommandUsingGpg(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
getGitConfigValue func(string) (string, error)
|
||||
test func(bool)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Option global and local config commit.gpgsign is not set",
|
||||
func(string) (string, error) { return "", nil },
|
||||
func(gpgEnabled bool) {
|
||||
assert.False(t, gpgEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Option commit.gpgsign is true",
|
||||
func(string) (string, error) {
|
||||
return "True", nil
|
||||
},
|
||||
func(gpgEnabled bool) {
|
||||
assert.True(t, gpgEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Option commit.gpgsign is on",
|
||||
func(string) (string, error) {
|
||||
return "ON", nil
|
||||
},
|
||||
func(gpgEnabled bool) {
|
||||
assert.True(t, gpgEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Option commit.gpgsign is yes",
|
||||
func(string) (string, error) {
|
||||
return "YeS", nil
|
||||
},
|
||||
func(gpgEnabled bool) {
|
||||
assert.True(t, gpgEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Option commit.gpgsign is 1",
|
||||
func(string) (string, error) {
|
||||
return "1", nil
|
||||
},
|
||||
func(gpgEnabled bool) {
|
||||
assert.True(t, gpgEnabled)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.getGitConfigValue = s.getGitConfigValue
|
||||
s.test(gitCmd.UsingGpg())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -15,12 +15,10 @@ func NewDummyGitCommand() *GitCommand {
|
||||
// NewDummyGitCommandWithOSCommand creates a new dummy GitCommand for testing
|
||||
func NewDummyGitCommandWithOSCommand(osCommand *oscommands.OSCommand) *GitCommand {
|
||||
return &GitCommand{
|
||||
Log: utils.NewDummyLog(),
|
||||
OSCommand: osCommand,
|
||||
Tr: i18n.NewTranslationSet(utils.NewDummyLog()),
|
||||
Config: config.NewDummyAppConfig(),
|
||||
getGlobalGitConfig: func(string) (string, error) { return "", nil },
|
||||
getLocalGitConfig: func(string) (string, error) { return "", nil },
|
||||
removeFile: func(string) error { return nil },
|
||||
Log: utils.NewDummyLog(),
|
||||
OSCommand: osCommand,
|
||||
Tr: i18n.NewTranslationSet(utils.NewDummyLog()),
|
||||
Config: config.NewDummyAppConfig(),
|
||||
getGitConfigValue: func(string) (string, error) { return "", nil },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,46 +2,53 @@ package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// CatFile obtains the content of a file
|
||||
func (c *GitCommand) CatFile(fileName string) (string, error) {
|
||||
return c.OSCommand.RunCommandWithOutput("%s %s", c.OSCommand.Platform.CatCmd, c.OSCommand.Quote(fileName))
|
||||
return c.OSCommand.CatFile(fileName)
|
||||
}
|
||||
|
||||
func (c *GitCommand) OpenMergeToolCmd() string {
|
||||
return "git mergetool"
|
||||
}
|
||||
|
||||
func (c *GitCommand) OpenMergeTool() error {
|
||||
return c.OSCommand.RunCommand("git mergetool")
|
||||
}
|
||||
|
||||
// StageFile stages a file
|
||||
func (c *GitCommand) StageFile(fileName string) error {
|
||||
// renamed files look like "file1 -> file2"
|
||||
fileNames := strings.Split(fileName, " -> ")
|
||||
return c.OSCommand.RunCommand("git add %s", c.OSCommand.Quote(fileNames[len(fileNames)-1]))
|
||||
return c.RunCommand("git add -- %s", c.OSCommand.Quote(fileName))
|
||||
}
|
||||
|
||||
// StageAll stages all files
|
||||
func (c *GitCommand) StageAll() error {
|
||||
return c.OSCommand.RunCommand("git add -A")
|
||||
return c.RunCommand("git add -A")
|
||||
}
|
||||
|
||||
// UnstageAll stages all files
|
||||
// UnstageAll unstages all files
|
||||
func (c *GitCommand) UnstageAll() error {
|
||||
return c.OSCommand.RunCommand("git reset")
|
||||
return c.RunCommand("git reset")
|
||||
}
|
||||
|
||||
// UnStageFile unstages a file
|
||||
func (c *GitCommand) UnStageFile(fileName string, tracked bool) error {
|
||||
command := "git rm --cached --force %s"
|
||||
if tracked {
|
||||
command = "git reset HEAD %s"
|
||||
// we accept an array of filenames for the cases where a file has been renamed i.e.
|
||||
// we accept the current name and the previous name
|
||||
func (c *GitCommand) UnStageFile(fileNames []string, reset bool) error {
|
||||
command := "git rm --cached --force -- %s"
|
||||
if reset {
|
||||
command = "git reset HEAD -- %s"
|
||||
}
|
||||
|
||||
// renamed files look like "file1 -> file2"
|
||||
fileNames := strings.Split(fileName, " -> ")
|
||||
for _, name := range fileNames {
|
||||
if err := c.OSCommand.RunCommand(command, c.OSCommand.Quote(name)); err != nil {
|
||||
return err
|
||||
@@ -56,20 +63,19 @@ func (c *GitCommand) BeforeAndAfterFileForRename(file *models.File) (*models.Fil
|
||||
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
|
||||
// we've got a file that represents a rename from one file to another. Here we will refetch
|
||||
// all files, passing the --no-renames flag and then recursively call the function
|
||||
// again for the before file and after file. At some point we should fix the abstraction itself
|
||||
// again for the before file and after file.
|
||||
|
||||
split := strings.Split(file.Name, " -> ")
|
||||
filesWithoutRenames := c.GetStatusFiles(GetStatusFileOptions{NoRenames: true})
|
||||
var beforeFile *models.File
|
||||
var afterFile *models.File
|
||||
for _, f := range filesWithoutRenames {
|
||||
if f.Name == split[0] {
|
||||
if f.Name == file.PreviousName {
|
||||
beforeFile = f
|
||||
}
|
||||
if f.Name == split[1] {
|
||||
|
||||
if f.Name == file.Name {
|
||||
afterFile = f
|
||||
}
|
||||
}
|
||||
@@ -105,24 +111,76 @@ func (c *GitCommand) DiscardAllFileChanges(file *models.File) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// if the file isn't tracked, we assume you want to delete it
|
||||
quotedFileName := c.OSCommand.Quote(file.Name)
|
||||
|
||||
if file.ShortStatus == "AA" {
|
||||
if err := c.RunCommand("git checkout --ours -- %s", quotedFileName); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.RunCommand("git add %s", quotedFileName); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if file.ShortStatus == "DU" {
|
||||
return c.RunCommand("git rm %s", quotedFileName)
|
||||
}
|
||||
|
||||
// if the file isn't tracked, we assume you want to delete it
|
||||
if file.HasStagedChanges || file.HasMergeConflicts {
|
||||
if err := c.OSCommand.RunCommand("git reset -- %s", quotedFileName); err != nil {
|
||||
if err := c.RunCommand("git reset -- %s", quotedFileName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !file.Tracked {
|
||||
return c.removeFile(file.Name)
|
||||
if file.ShortStatus == "DD" || file.ShortStatus == "AU" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if file.Added {
|
||||
return c.OSCommand.RemoveFile(file.Name)
|
||||
}
|
||||
return c.DiscardUnstagedFileChanges(file)
|
||||
}
|
||||
|
||||
func (c *GitCommand) DiscardAllDirChanges(node *filetree.FileNode) error {
|
||||
// this could be more efficient but we would need to handle all the edge cases
|
||||
return node.ForEachFile(c.DiscardAllFileChanges)
|
||||
}
|
||||
|
||||
func (c *GitCommand) DiscardUnstagedDirChanges(node *filetree.FileNode) error {
|
||||
if err := c.RemoveUntrackedDirFiles(node); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
quotedPath := c.OSCommand.Quote(node.GetPath())
|
||||
if err := c.RunCommand("git checkout -- %s", quotedPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *GitCommand) RemoveUntrackedDirFiles(node *filetree.FileNode) error {
|
||||
untrackedFilePaths := node.GetPathsMatching(
|
||||
func(n *filetree.FileNode) bool { return n.File != nil && !n.File.GetIsTracked() },
|
||||
)
|
||||
|
||||
for _, path := range untrackedFilePaths {
|
||||
err := os.Remove(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DiscardUnstagedFileChanges directly
|
||||
func (c *GitCommand) DiscardUnstagedFileChanges(file *models.File) error {
|
||||
quotedFileName := c.OSCommand.Quote(file.Name)
|
||||
return c.OSCommand.RunCommand("git checkout -- %s", quotedFileName)
|
||||
return c.RunCommand("git checkout -- %s", quotedFileName)
|
||||
}
|
||||
|
||||
// Ignore adds a file to the gitignore for the repo
|
||||
@@ -137,23 +195,22 @@ func (c *GitCommand) WorktreeFileDiff(file *models.File, plain bool, cached bool
|
||||
return s
|
||||
}
|
||||
|
||||
func (c *GitCommand) WorktreeFileDiffCmdStr(file *models.File, plain bool, cached bool) string {
|
||||
func (c *GitCommand) WorktreeFileDiffCmdStr(node models.IFile, plain bool, cached bool) string {
|
||||
cachedArg := ""
|
||||
trackedArg := "--"
|
||||
colorArg := c.colorArg()
|
||||
split := strings.Split(file.Name, " -> ") // in case of a renamed file we get the new filename
|
||||
fileName := c.OSCommand.Quote(split[len(split)-1])
|
||||
path := c.OSCommand.Quote(node.GetPath())
|
||||
if cached {
|
||||
cachedArg = "--cached"
|
||||
}
|
||||
if !file.Tracked && !file.HasStagedChanges && !cached {
|
||||
trackedArg = "--no-index /dev/null"
|
||||
if !node.GetIsTracked() && !node.GetHasStagedChanges() && !cached {
|
||||
trackedArg = "--no-index -- /dev/null"
|
||||
}
|
||||
if plain {
|
||||
colorArg = "never"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("git diff --submodule --no-ext-diff --color=%s %s %s %s", colorArg, cachedArg, trackedArg, fileName)
|
||||
return fmt.Sprintf("git diff --submodule --no-ext-diff --color=%s %s %s %s", colorArg, cachedArg, trackedArg, path)
|
||||
}
|
||||
|
||||
func (c *GitCommand) ApplyPatch(patch string, flags ...string) error {
|
||||
@@ -168,7 +225,7 @@ func (c *GitCommand) ApplyPatch(patch string, flags ...string) error {
|
||||
flagStr += " --" + flag
|
||||
}
|
||||
|
||||
return c.OSCommand.RunCommand("git apply %s %s", flagStr, c.OSCommand.Quote(filepath))
|
||||
return c.RunCommand("git apply %s %s", flagStr, c.OSCommand.Quote(filepath))
|
||||
}
|
||||
|
||||
// ShowFileDiff get the diff of specified from and to. Typically this will be used for a single commit so it'll be 123abc^..123abc
|
||||
@@ -194,7 +251,7 @@ func (c *GitCommand) ShowFileDiffCmdStr(from string, to string, reverse bool, fi
|
||||
|
||||
// CheckoutFile checks out the file for the given commit
|
||||
func (c *GitCommand) CheckoutFile(commitSha, fileName string) error {
|
||||
return c.OSCommand.RunCommand("git checkout %s %s", commitSha, fileName)
|
||||
return c.RunCommand("git checkout %s %s", commitSha, fileName)
|
||||
}
|
||||
|
||||
// DiscardOldFileChanges discards changes to a file from an old commit
|
||||
@@ -204,7 +261,7 @@ func (c *GitCommand) DiscardOldFileChanges(commits []*models.Commit, commitIndex
|
||||
}
|
||||
|
||||
// check if file exists in previous commit (this command returns an error if the file doesn't exist)
|
||||
if err := c.OSCommand.RunCommand("git cat-file -e HEAD^:%s", fileName); err != nil {
|
||||
if err := c.RunCommand("git cat-file -e HEAD^:%s", fileName); err != nil {
|
||||
if err := c.OSCommand.Remove(fileName); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -216,10 +273,7 @@ func (c *GitCommand) DiscardOldFileChanges(commits []*models.Commit, commitIndex
|
||||
}
|
||||
|
||||
// amend the commit
|
||||
cmd, err := c.AmendHead()
|
||||
if cmd != nil {
|
||||
return errors.New("received unexpected pointer to cmd")
|
||||
}
|
||||
err := c.AmendHead()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -230,17 +284,17 @@ func (c *GitCommand) DiscardOldFileChanges(commits []*models.Commit, commitIndex
|
||||
|
||||
// DiscardAnyUnstagedFileChanges discards any unstages file changes via `git checkout -- .`
|
||||
func (c *GitCommand) DiscardAnyUnstagedFileChanges() error {
|
||||
return c.OSCommand.RunCommand("git checkout -- .")
|
||||
return c.RunCommand("git checkout -- .")
|
||||
}
|
||||
|
||||
// RemoveTrackedFiles will delete the given file(s) even if they are currently tracked
|
||||
func (c *GitCommand) RemoveTrackedFiles(name string) error {
|
||||
return c.OSCommand.RunCommand("git rm -r --cached %s", name)
|
||||
return c.RunCommand("git rm -r --cached %s", name)
|
||||
}
|
||||
|
||||
// RemoveUntrackedFiles runs `git clean -fd`
|
||||
func (c *GitCommand) RemoveUntrackedFiles() error {
|
||||
return c.OSCommand.RunCommand("git clean -fd")
|
||||
return c.RunCommand("git clean -fd")
|
||||
}
|
||||
|
||||
// ResetAndClean removes all unstaged changes and removes all untracked files
|
||||
@@ -262,3 +316,27 @@ func (c *GitCommand) ResetAndClean() error {
|
||||
|
||||
return c.RemoveUntrackedFiles()
|
||||
}
|
||||
|
||||
func (c *GitCommand) EditFileCmdStr(filename string) (string, error) {
|
||||
editor := c.GetConfigValue("core.editor")
|
||||
|
||||
if editor == "" {
|
||||
editor = c.OSCommand.Getenv("GIT_EDITOR")
|
||||
}
|
||||
if editor == "" {
|
||||
editor = c.OSCommand.Getenv("VISUAL")
|
||||
}
|
||||
if editor == "" {
|
||||
editor = c.OSCommand.Getenv("EDITOR")
|
||||
}
|
||||
if editor == "" {
|
||||
if err := c.OSCommand.RunCommand("which vi"); err == nil {
|
||||
editor = "vi"
|
||||
}
|
||||
}
|
||||
if editor == "" {
|
||||
return "", errors.New("No editor defined in $GIT_EDITOR, $VISUAL, $EDITOR, or git config")
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %s", editor, c.OSCommand.Quote(filename)), nil
|
||||
}
|
||||
|
||||
846
pkg/commands/files_test.go
Normal file
846
pkg/commands/files_test.go
Normal file
@@ -0,0 +1,846 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// 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, osCmd, cmd)
|
||||
assert.EqualValues(t, []string{"test.txt"}, args)
|
||||
|
||||
return secureexec.Command("echo", "-n", "test")
|
||||
}
|
||||
|
||||
o, err := gitCmd.CatFile("test.txt")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test", o)
|
||||
}
|
||||
|
||||
// TestGitCommandStageFile is a function.
|
||||
func TestGitCommandStageFile(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"add", "--", "test.txt"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.StageFile("test.txt"))
|
||||
}
|
||||
|
||||
// TestGitCommandUnstageFile is a function.
|
||||
func TestGitCommandUnstageFile(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
reset bool
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Remove an untracked file from staging",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"rm", "--cached", "--force", "--", "test.txt"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Remove a tracked file from staging",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"reset", "HEAD", "--", "test.txt"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.UnStageFile([]string{"test.txt"}, s.reset))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandDiscardAllFileChanges is a function.
|
||||
// these tests don't cover everything, in part because we already have an integration
|
||||
// test which does cover everything. I don't want to unnecessarily assert on the 'how'
|
||||
// when the 'what' is what matters
|
||||
func TestGitCommandDiscardAllFileChanges(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func() (func(string, ...string) *exec.Cmd, *[][]string)
|
||||
test func(*[][]string, error)
|
||||
file *models.File
|
||||
removeFile func(string) error
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"An error occurred when resetting",
|
||||
func() (func(string, ...string) *exec.Cmd, *[][]string) {
|
||||
cmdsCalled := [][]string{}
|
||||
return func(cmd string, args ...string) *exec.Cmd {
|
||||
cmdsCalled = append(cmdsCalled, args)
|
||||
|
||||
return secureexec.Command("test")
|
||||
}, &cmdsCalled
|
||||
},
|
||||
func(cmdsCalled *[][]string, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.Len(t, *cmdsCalled, 1)
|
||||
assert.EqualValues(t, *cmdsCalled, [][]string{
|
||||
{"reset", "--", "test"},
|
||||
})
|
||||
},
|
||||
&models.File{
|
||||
Name: "test",
|
||||
HasStagedChanges: true,
|
||||
},
|
||||
func(string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
"An error occurred when removing file",
|
||||
func() (func(string, ...string) *exec.Cmd, *[][]string) {
|
||||
cmdsCalled := [][]string{}
|
||||
return func(cmd string, args ...string) *exec.Cmd {
|
||||
cmdsCalled = append(cmdsCalled, args)
|
||||
|
||||
return secureexec.Command("test")
|
||||
}, &cmdsCalled
|
||||
},
|
||||
func(cmdsCalled *[][]string, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.EqualError(t, err, "an error occurred when removing file")
|
||||
assert.Len(t, *cmdsCalled, 0)
|
||||
},
|
||||
&models.File{
|
||||
Name: "test",
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
},
|
||||
func(string) error {
|
||||
return fmt.Errorf("an error occurred when removing file")
|
||||
},
|
||||
},
|
||||
{
|
||||
"An error occurred with checkout",
|
||||
func() (func(string, ...string) *exec.Cmd, *[][]string) {
|
||||
cmdsCalled := [][]string{}
|
||||
return func(cmd string, args ...string) *exec.Cmd {
|
||||
cmdsCalled = append(cmdsCalled, args)
|
||||
|
||||
return secureexec.Command("test")
|
||||
}, &cmdsCalled
|
||||
},
|
||||
func(cmdsCalled *[][]string, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.Len(t, *cmdsCalled, 1)
|
||||
assert.EqualValues(t, *cmdsCalled, [][]string{
|
||||
{"checkout", "--", "test"},
|
||||
})
|
||||
},
|
||||
&models.File{
|
||||
Name: "test",
|
||||
Tracked: true,
|
||||
HasStagedChanges: false,
|
||||
},
|
||||
func(string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
"Checkout only",
|
||||
func() (func(string, ...string) *exec.Cmd, *[][]string) {
|
||||
cmdsCalled := [][]string{}
|
||||
return func(cmd string, args ...string) *exec.Cmd {
|
||||
cmdsCalled = append(cmdsCalled, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}, &cmdsCalled
|
||||
},
|
||||
func(cmdsCalled *[][]string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, *cmdsCalled, 1)
|
||||
assert.EqualValues(t, *cmdsCalled, [][]string{
|
||||
{"checkout", "--", "test"},
|
||||
})
|
||||
},
|
||||
&models.File{
|
||||
Name: "test",
|
||||
Tracked: true,
|
||||
HasStagedChanges: false,
|
||||
},
|
||||
func(string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
"Reset and checkout staged changes",
|
||||
func() (func(string, ...string) *exec.Cmd, *[][]string) {
|
||||
cmdsCalled := [][]string{}
|
||||
return func(cmd string, args ...string) *exec.Cmd {
|
||||
cmdsCalled = append(cmdsCalled, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}, &cmdsCalled
|
||||
},
|
||||
func(cmdsCalled *[][]string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, *cmdsCalled, 2)
|
||||
assert.EqualValues(t, *cmdsCalled, [][]string{
|
||||
{"reset", "--", "test"},
|
||||
{"checkout", "--", "test"},
|
||||
})
|
||||
},
|
||||
&models.File{
|
||||
Name: "test",
|
||||
Tracked: true,
|
||||
HasStagedChanges: true,
|
||||
},
|
||||
func(string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
"Reset and checkout merge conflicts",
|
||||
func() (func(string, ...string) *exec.Cmd, *[][]string) {
|
||||
cmdsCalled := [][]string{}
|
||||
return func(cmd string, args ...string) *exec.Cmd {
|
||||
cmdsCalled = append(cmdsCalled, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}, &cmdsCalled
|
||||
},
|
||||
func(cmdsCalled *[][]string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, *cmdsCalled, 2)
|
||||
assert.EqualValues(t, *cmdsCalled, [][]string{
|
||||
{"reset", "--", "test"},
|
||||
{"checkout", "--", "test"},
|
||||
})
|
||||
},
|
||||
&models.File{
|
||||
Name: "test",
|
||||
Tracked: true,
|
||||
HasMergeConflicts: true,
|
||||
},
|
||||
func(string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
"Reset and remove",
|
||||
func() (func(string, ...string) *exec.Cmd, *[][]string) {
|
||||
cmdsCalled := [][]string{}
|
||||
return func(cmd string, args ...string) *exec.Cmd {
|
||||
cmdsCalled = append(cmdsCalled, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}, &cmdsCalled
|
||||
},
|
||||
func(cmdsCalled *[][]string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, *cmdsCalled, 1)
|
||||
assert.EqualValues(t, *cmdsCalled, [][]string{
|
||||
{"reset", "--", "test"},
|
||||
})
|
||||
},
|
||||
&models.File{
|
||||
Name: "test",
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
HasStagedChanges: true,
|
||||
},
|
||||
func(filename string) error {
|
||||
assert.Equal(t, "test", filename)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
"Remove only",
|
||||
func() (func(string, ...string) *exec.Cmd, *[][]string) {
|
||||
cmdsCalled := [][]string{}
|
||||
return func(cmd string, args ...string) *exec.Cmd {
|
||||
cmdsCalled = append(cmdsCalled, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}, &cmdsCalled
|
||||
},
|
||||
func(cmdsCalled *[][]string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, *cmdsCalled, 0)
|
||||
},
|
||||
&models.File{
|
||||
Name: "test",
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
HasStagedChanges: false,
|
||||
},
|
||||
func(filename string) error {
|
||||
assert.Equal(t, "test", filename)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
var cmdsCalled *[][]string
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command, cmdsCalled = s.command()
|
||||
gitCmd.OSCommand.SetRemoveFile(s.removeFile)
|
||||
s.test(cmdsCalled, gitCmd.DiscardAllFileChanges(s.file))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandDiff is a function.
|
||||
func TestGitCommandDiff(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
file *models.File
|
||||
plain bool
|
||||
cached bool
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Default case",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=always", "--", "test.txt"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
&models.File{
|
||||
Name: "test.txt",
|
||||
HasStagedChanges: false,
|
||||
Tracked: true,
|
||||
},
|
||||
false,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"cached",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=always", "--cached", "--", "test.txt"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
&models.File{
|
||||
Name: "test.txt",
|
||||
HasStagedChanges: false,
|
||||
Tracked: true,
|
||||
},
|
||||
false,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"plain",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=never", "--", "test.txt"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
&models.File{
|
||||
Name: "test.txt",
|
||||
HasStagedChanges: false,
|
||||
Tracked: true,
|
||||
},
|
||||
true,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"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", "--submodule", "--no-ext-diff", "--color=always", "--no-index", "--", "/dev/null", "test.txt"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
&models.File{
|
||||
Name: "test.txt",
|
||||
HasStagedChanges: false,
|
||||
Tracked: false,
|
||||
},
|
||||
false,
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
gitCmd.WorktreeFileDiff(s.file, s.plain, s.cached)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandCheckoutFile is a function.
|
||||
func TestGitCommandCheckoutFile(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
commitSha string
|
||||
fileName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"typical case",
|
||||
"11af912",
|
||||
"test999.txt",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git checkout 11af912 test999.txt",
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"returns error if there is one",
|
||||
"11af912",
|
||||
"test999.txt",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git checkout 11af912 test999.txt",
|
||||
Replace: "test",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.CheckoutFile(s.commitSha, s.fileName))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitCommandApplyPatch(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.Equal(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"apply", "--cached"}, args[0:2])
|
||||
filename := args[2]
|
||||
content, err := ioutil.ReadFile(filename)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "test", string(content))
|
||||
|
||||
return secureexec.Command("echo", "done")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"command returns error",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.Equal(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"apply", "--cached"}, args[0:2])
|
||||
filename := args[2]
|
||||
// TODO: Ideally we want to mock out OSCommand here so that we're not
|
||||
// double handling testing it's CreateTempFile functionality,
|
||||
// but it is going to take a bit of work to make a proper mock for it
|
||||
// so I'm leaving it for another PR
|
||||
content, err := ioutil.ReadFile(filename)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "test", string(content))
|
||||
|
||||
return secureexec.Command("test")
|
||||
},
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.ApplyPatch("test", "cached"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandDiscardOldFileChanges is a function.
|
||||
func TestGitCommandDiscardOldFileChanges(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
getGitConfigValue func(string) (string, error)
|
||||
commits []*models.Commit
|
||||
commitIndex int
|
||||
fileName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"returns error when index outside of range of commits",
|
||||
func(string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
[]*models.Commit{},
|
||||
0,
|
||||
"test999.txt",
|
||||
nil,
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"returns error when using gpg",
|
||||
func(string) (string, error) {
|
||||
return "true", nil
|
||||
},
|
||||
[]*models.Commit{{Name: "commit", Sha: "123456"}},
|
||||
0,
|
||||
"test999.txt",
|
||||
nil,
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"checks out file if it already existed",
|
||||
func(string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
[]*models.Commit{
|
||||
{Name: "commit", Sha: "123456"},
|
||||
{Name: "commit2", Sha: "abcdef"},
|
||||
},
|
||||
0,
|
||||
"test999.txt",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git rebase --interactive --autostash --keep-empty abcdef",
|
||||
Replace: "echo",
|
||||
},
|
||||
{
|
||||
Expect: "git cat-file -e HEAD^:test999.txt",
|
||||
Replace: "echo",
|
||||
},
|
||||
{
|
||||
Expect: "git checkout HEAD^ test999.txt",
|
||||
Replace: "echo",
|
||||
},
|
||||
{
|
||||
Expect: "git commit --amend --no-edit --allow-empty",
|
||||
Replace: "echo",
|
||||
},
|
||||
{
|
||||
Expect: "git rebase --continue",
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
// test for when the file was created within the commit requires a refactor to support proper mocks
|
||||
// currently we'd need to mock out the os.Remove function and that's gonna introduce tech debt
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
gitCmd.getGitConfigValue = s.getGitConfigValue
|
||||
s.test(gitCmd.DiscardOldFileChanges(s.commits, s.commitIndex, s.fileName))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandDiscardUnstagedFileChanges is a function.
|
||||
func TestGitCommandDiscardUnstagedFileChanges(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
file *models.File
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
&models.File{Name: "test.txt"},
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: `git checkout -- "test.txt"`,
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.DiscardUnstagedFileChanges(s.file))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandDiscardAnyUnstagedFileChanges is a function.
|
||||
func TestGitCommandDiscardAnyUnstagedFileChanges(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: `git checkout -- .`,
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.DiscardAnyUnstagedFileChanges())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandRemoveUntrackedFiles is a function.
|
||||
func TestGitCommandRemoveUntrackedFiles(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: `git clean -fd`,
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.RemoveUntrackedFiles())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestEditFileCmdStr is a function.
|
||||
func TestEditFileCmdStr(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
getenv func(string) string
|
||||
getGitConfigValue func(string) (string, error)
|
||||
test func(string, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
return secureexec.Command("exit", "1")
|
||||
},
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmdStr string, err error) {
|
||||
assert.EqualError(t, err, "No editor defined in $GIT_EDITOR, $VISUAL, $EDITOR, or git config")
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "which", name)
|
||||
return secureexec.Command("exit", "1")
|
||||
},
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "nano", nil
|
||||
},
|
||||
func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "nano \"test\"", cmdStr)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "which", name)
|
||||
return secureexec.Command("exit", "1")
|
||||
},
|
||||
func(env string) string {
|
||||
if env == "VISUAL" {
|
||||
return "nano"
|
||||
}
|
||||
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "which", name)
|
||||
return secureexec.Command("exit", "1")
|
||||
},
|
||||
func(env string) string {
|
||||
if env == "EDITOR" {
|
||||
return "emacs"
|
||||
}
|
||||
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "emacs \"test\"", cmdStr)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "which", name)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "vi \"test\"", cmdStr)
|
||||
},
|
||||
},
|
||||
{
|
||||
"file/with space",
|
||||
func(name string, args ...string) *exec.Cmd {
|
||||
assert.Equal(t, "which", name)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "vi \"file/with space\"", cmdStr)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
gitCmd.OSCommand.Getenv = s.getenv
|
||||
gitCmd.getGitConfigValue = s.getGitConfigValue
|
||||
s.test(gitCmd.EditFileCmdStr(s.filename))
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
@@ -16,7 +17,6 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
gitconfig "github.com/tcnksm/go-gitconfig"
|
||||
)
|
||||
|
||||
// this takes something like:
|
||||
@@ -32,9 +32,7 @@ type GitCommand struct {
|
||||
Repo *gogit.Repository
|
||||
Tr *i18n.TranslationSet
|
||||
Config config.AppConfigurer
|
||||
getGlobalGitConfig func(string) (string, error)
|
||||
getLocalGitConfig func(string) (string, error)
|
||||
removeFile func(string) error
|
||||
getGitConfigValue func(string) (string, error)
|
||||
DotGitDir string
|
||||
onSuccessfulContinue func() error
|
||||
PatchManager *patch.PatchManager
|
||||
@@ -56,10 +54,6 @@ func NewGitCommand(log *logrus.Entry, osCommand *oscommands.OSCommand, tr *i18n.
|
||||
pushToCurrent = strings.TrimSpace(output) == "current"
|
||||
}
|
||||
|
||||
if err := verifyInGitRepo(osCommand.RunCommand); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := navigateToRepoRootDirectory(os.Stat, os.Chdir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -74,16 +68,14 @@ func NewGitCommand(log *logrus.Entry, osCommand *oscommands.OSCommand, tr *i18n.
|
||||
}
|
||||
|
||||
gitCommand := &GitCommand{
|
||||
Log: log,
|
||||
OSCommand: osCommand,
|
||||
Tr: tr,
|
||||
Repo: repo,
|
||||
Config: config,
|
||||
getGlobalGitConfig: gitconfig.Global,
|
||||
getLocalGitConfig: gitconfig.Local,
|
||||
removeFile: os.RemoveAll,
|
||||
DotGitDir: dotGitDir,
|
||||
PushToCurrent: pushToCurrent,
|
||||
Log: log,
|
||||
OSCommand: osCommand,
|
||||
Tr: tr,
|
||||
Repo: repo,
|
||||
Config: config,
|
||||
getGitConfigValue: getGitConfigValue,
|
||||
DotGitDir: dotGitDir,
|
||||
PushToCurrent: pushToCurrent,
|
||||
}
|
||||
|
||||
gitCommand.PatchManager = patch.NewPatchManager(log, gitCommand.ApplyPatch, gitCommand.ShowFileDiff)
|
||||
@@ -91,8 +83,25 @@ func NewGitCommand(log *logrus.Entry, osCommand *oscommands.OSCommand, tr *i18n.
|
||||
return gitCommand, nil
|
||||
}
|
||||
|
||||
func verifyInGitRepo(runCmd func(string, ...interface{}) error) error {
|
||||
return runCmd("git status")
|
||||
func (c *GitCommand) WithSpan(span string) *GitCommand {
|
||||
// sometimes .WithSpan(span) will be called where span actually is empty, in
|
||||
// which case we don't need to log anything so we can just return early here
|
||||
// with the original struct
|
||||
if span == "" {
|
||||
return c
|
||||
}
|
||||
|
||||
newGitCommand := &GitCommand{}
|
||||
*newGitCommand = *c
|
||||
newGitCommand.OSCommand = c.OSCommand.WithSpan(span)
|
||||
|
||||
// NOTE: unlike the other things here which create shallow clones, this will
|
||||
// actually update the PatchManager on the original struct to have the new span.
|
||||
// This means each time we call ApplyPatch in PatchManager, we need to ensure
|
||||
// we've called .WithSpan() ahead of time with the new span value
|
||||
newGitCommand.PatchManager.ApplyPatch = newGitCommand.ApplyPatch
|
||||
|
||||
return newGitCommand
|
||||
}
|
||||
|
||||
func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir func(string) error) error {
|
||||
@@ -123,6 +132,18 @@ func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir f
|
||||
if err = chdir(".."); err != nil {
|
||||
return utils.WrapError(err)
|
||||
}
|
||||
|
||||
currentPath, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
atRoot := currentPath == filepath.Dir(currentPath)
|
||||
if atRoot {
|
||||
// we should never really land here: the code that creates GitCommand should
|
||||
// verify we're in a git directory
|
||||
return errors.New("Must open lazygit in a git repository")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,3 +213,36 @@ func findDotGitDir(stat func(string) (os.FileInfo, error), readFile func(filenam
|
||||
}
|
||||
return strings.TrimSpace(strings.TrimPrefix(fileContent, "gitdir: ")), nil
|
||||
}
|
||||
|
||||
func VerifyInGitRepo(osCommand *oscommands.OSCommand) error {
|
||||
return osCommand.RunCommand("git rev-parse --git-dir")
|
||||
}
|
||||
|
||||
func (c *GitCommand) RunCommand(formatString string, formatArgs ...interface{}) error {
|
||||
_, err := c.RunCommandWithOutput(formatString, formatArgs...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *GitCommand) RunCommandWithOutput(formatString string, formatArgs ...interface{}) (string, error) {
|
||||
// TODO: have this retry logic in other places we run the command
|
||||
waitTime := 50 * time.Millisecond
|
||||
retryCount := 5
|
||||
attempt := 0
|
||||
|
||||
for {
|
||||
output, err := c.OSCommand.RunCommandWithOutput(formatString, formatArgs...)
|
||||
if err != nil {
|
||||
// if we have an error based on the index lock, we should wait a bit and then retry
|
||||
if strings.Contains(output, ".git/index.lock") {
|
||||
c.Log.Error(output)
|
||||
c.Log.Info("index.lock prevented command from running. Retrying command after a small wait")
|
||||
attempt++
|
||||
time.Sleep(waitTime)
|
||||
if attempt < retryCount {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
return output, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package commands
|
||||
|
||||
// Conflict : A git conflict with a start middle and end corresponding to line
|
||||
// numbers in the file where the conflict bars appear
|
||||
type Conflict struct {
|
||||
Start int
|
||||
Middle int
|
||||
End int
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
56
pkg/commands/gitconfig.go
Normal file
56
pkg/commands/gitconfig.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
)
|
||||
|
||||
// including license from https://github.com/tcnksm/go-gitconfig because this file is an adaptation of that repo's code
|
||||
// Copyright (c) 2014 tcnksm
|
||||
|
||||
// MIT License
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining
|
||||
// a copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to
|
||||
// permit persons to whom the Software is furnished to do so, subject to
|
||||
// the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
func getGitConfigValue(key string) (string, error) {
|
||||
gitArgs := []string{"config", "--get", "--null", key}
|
||||
var stdout bytes.Buffer
|
||||
cmd := secureexec.Command("git", gitArgs...)
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = ioutil.Discard
|
||||
|
||||
err := cmd.Run()
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
if waitStatus, ok := exitError.Sys().(syscall.WaitStatus); ok {
|
||||
if waitStatus.ExitStatus() == 1 {
|
||||
return "", fmt.Errorf("the key `%s` is not found", key)
|
||||
}
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
return strings.TrimRight(stdout.String(), "\000"), nil
|
||||
}
|
||||
@@ -4,45 +4,37 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
)
|
||||
|
||||
// GetFilesInDiff get the specified commit files
|
||||
func (c *GitCommand) GetFilesInDiff(from string, to string, reverse bool, patchManager *patch.PatchManager) ([]*models.CommitFile, error) {
|
||||
func (c *GitCommand) GetFilesInDiff(from string, to string, reverse bool) ([]*models.CommitFile, error) {
|
||||
reverseFlag := ""
|
||||
if reverse {
|
||||
reverseFlag = " -R "
|
||||
}
|
||||
|
||||
filenames, err := c.OSCommand.RunCommandWithOutput("git diff --submodule --no-ext-diff --name-status %s %s %s", reverseFlag, from, to)
|
||||
filenames, err := c.RunCommandWithOutput("git diff --submodule --no-ext-diff --name-status -z --no-renames %s %s %s", reverseFlag, from, to)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.getCommitFilesFromFilenames(filenames, to, patchManager), nil
|
||||
return c.getCommitFilesFromFilenames(filenames), nil
|
||||
}
|
||||
|
||||
// filenames string is something like "file1\nfile2\nfile3"
|
||||
func (c *GitCommand) getCommitFilesFromFilenames(filenames string, parent string, patchManager *patch.PatchManager) []*models.CommitFile {
|
||||
func (c *GitCommand) getCommitFilesFromFilenames(filenames string) []*models.CommitFile {
|
||||
commitFiles := make([]*models.CommitFile, 0)
|
||||
|
||||
for _, line := range strings.Split(strings.TrimRight(filenames, "\n"), "\n") {
|
||||
lines := strings.Split(strings.TrimRight(filenames, "\x00"), "\x00")
|
||||
n := len(lines)
|
||||
for i := 0; i < n-1; i += 2 {
|
||||
// typical result looks like 'A my_file' meaning my_file was added
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
changeStatus := line[0:1]
|
||||
name := line[2:]
|
||||
status := patch.UNSELECTED
|
||||
if patchManager != nil && patchManager.To == parent {
|
||||
status = patchManager.GetFileStatus(name)
|
||||
}
|
||||
changeStatus := lines[i]
|
||||
name := lines[i+1]
|
||||
|
||||
commitFiles = append(commitFiles, &models.CommitFile{
|
||||
Parent: parent,
|
||||
Name: name,
|
||||
ChangeStatus: changeStatus,
|
||||
PatchStatus: status,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -185,9 +185,9 @@ func (c *CommitListBuilder) GetCommits(opts GetCommitsOptions) ([]*models.Commit
|
||||
// getRebasingCommits obtains the commits that we're in the process of rebasing
|
||||
func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*models.Commit, error) {
|
||||
switch rebaseMode {
|
||||
case "normal":
|
||||
case REBASE_MODE_MERGING:
|
||||
return c.getNormalRebasingCommits()
|
||||
case "interactive":
|
||||
case REBASE_MODE_INTERACTIVE:
|
||||
return c.getInteractiveRebasingCommits()
|
||||
default:
|
||||
return nil, nil
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -39,10 +40,10 @@ func TestCommitListBuilderGetMergeBase(t *testing.T) {
|
||||
switch args[0] {
|
||||
case "symbolic-ref":
|
||||
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
|
||||
return exec.Command("echo", "master")
|
||||
return secureexec.Command("echo", "master")
|
||||
case "merge-base":
|
||||
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
|
||||
return exec.Command("test")
|
||||
return secureexec.Command("test")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
@@ -59,10 +60,10 @@ func TestCommitListBuilderGetMergeBase(t *testing.T) {
|
||||
switch args[0] {
|
||||
case "symbolic-ref":
|
||||
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
|
||||
return exec.Command("echo", "master")
|
||||
return secureexec.Command("echo", "master")
|
||||
case "merge-base":
|
||||
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
|
||||
return exec.Command("echo", "blah")
|
||||
return secureexec.Command("echo", "blah")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
@@ -79,10 +80,10 @@ func TestCommitListBuilderGetMergeBase(t *testing.T) {
|
||||
switch args[0] {
|
||||
case "symbolic-ref":
|
||||
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
|
||||
return exec.Command("echo", "feature/test")
|
||||
return secureexec.Command("echo", "feature/test")
|
||||
case "merge-base":
|
||||
assert.EqualValues(t, []string{"merge-base", "HEAD", "develop"}, args)
|
||||
return exec.Command("echo", "blah")
|
||||
return secureexec.Command("echo", "blah")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
@@ -94,7 +95,7 @@ func TestCommitListBuilderGetMergeBase(t *testing.T) {
|
||||
{
|
||||
"bubbles up error if there is one",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return exec.Command("test")
|
||||
return secureexec.Command("test")
|
||||
},
|
||||
func(output string, err error) {
|
||||
assert.Error(t, err)
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
const RENAME_SEPARATOR = " -> "
|
||||
|
||||
// GetStatusFiles git status files
|
||||
type GetStatusFileOptions struct {
|
||||
NoRenames bool
|
||||
@@ -37,26 +39,35 @@ func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
|
||||
change := statusString[0:2]
|
||||
stagedChange := change[0:1]
|
||||
unstagedChange := statusString[1:2]
|
||||
filename := c.OSCommand.Unquote(statusString[3:])
|
||||
name := statusString[3:]
|
||||
untracked := utils.IncludesString([]string{"??", "A ", "AM"}, change)
|
||||
hasNoStagedChanges := utils.IncludesString([]string{" ", "U", "?"}, stagedChange)
|
||||
hasMergeConflicts := utils.IncludesString([]string{"DD", "AA", "UU", "AU", "UA", "UD", "DU"}, change)
|
||||
hasInlineMergeConflicts := utils.IncludesString([]string{"UU", "AA"}, change)
|
||||
previousName := ""
|
||||
if strings.Contains(name, RENAME_SEPARATOR) {
|
||||
split := strings.Split(name, RENAME_SEPARATOR)
|
||||
name = split[1]
|
||||
previousName = split[0]
|
||||
}
|
||||
|
||||
file := &models.File{
|
||||
Name: filename,
|
||||
Name: name,
|
||||
PreviousName: previousName,
|
||||
DisplayString: statusString,
|
||||
HasStagedChanges: !hasNoStagedChanges,
|
||||
HasUnstagedChanges: unstagedChange != " ",
|
||||
Tracked: !untracked,
|
||||
Deleted: unstagedChange == "D" || stagedChange == "D",
|
||||
Added: unstagedChange == "A" || untracked,
|
||||
HasMergeConflicts: hasMergeConflicts,
|
||||
HasInlineMergeConflicts: hasInlineMergeConflicts,
|
||||
Type: c.OSCommand.FileType(filename),
|
||||
Type: c.OSCommand.FileType(name),
|
||||
ShortStatus: change,
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
@@ -72,40 +83,22 @@ func (c *GitCommand) GitStatus(opts GitStatusOptions) (string, error) {
|
||||
noRenamesFlag = "--no-renames"
|
||||
}
|
||||
|
||||
return c.OSCommand.RunCommandWithOutput("git status %s --porcelain %s", opts.UntrackedFilesArg, noRenamesFlag)
|
||||
}
|
||||
|
||||
// MergeStatusFiles merge status files
|
||||
func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []*models.File, selectedFile *models.File) []*models.File {
|
||||
if len(oldFiles) == 0 {
|
||||
return newFiles
|
||||
statusLines, err := c.RunCommandWithOutput("git status %s --porcelain -z %s", opts.UntrackedFilesArg, noRenamesFlag)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
appendedIndexes := []int{}
|
||||
|
||||
// retain position of files we already could see
|
||||
result := []*models.File{}
|
||||
for _, oldFile := range oldFiles {
|
||||
for newIndex, newFile := range newFiles {
|
||||
if utils.IncludesInt(appendedIndexes, newIndex) {
|
||||
continue
|
||||
}
|
||||
// if we just staged B and in doing so created 'A -> B' and we are currently have oldFile: A and newFile: 'A -> B', we want to wait until we come across B so the our cursor isn't jumping anywhere
|
||||
waitForMatchingFile := selectedFile != nil && newFile.IsRename() && !selectedFile.IsRename() && newFile.Matches(selectedFile) && !oldFile.Matches(selectedFile)
|
||||
|
||||
if oldFile.Matches(newFile) && !waitForMatchingFile {
|
||||
result = append(result, newFile)
|
||||
appendedIndexes = append(appendedIndexes, newIndex)
|
||||
}
|
||||
splitLines := strings.Split(statusLines, "\x00")
|
||||
// if a line starts with 'R' then the next line is the original file.
|
||||
for i := 0; i < len(splitLines)-1; i++ {
|
||||
original := splitLines[i]
|
||||
if strings.HasPrefix(original, "R ") {
|
||||
next := splitLines[i+1]
|
||||
updated := "R " + next + RENAME_SEPARATOR + strings.TrimPrefix(original, "R ")
|
||||
splitLines[i] = updated
|
||||
splitLines = append(splitLines[0:i+1], splitLines[i+2:]...)
|
||||
}
|
||||
}
|
||||
|
||||
// append any new files to the end
|
||||
for index, newFile := range newFiles {
|
||||
if !utils.IncludesInt(appendedIndexes, index) {
|
||||
result = append(result, newFile)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
return strings.Join(splitLines, "\n"), nil
|
||||
}
|
||||
|
||||
122
pkg/commands/loading_files_test.go
Normal file
122
pkg/commands/loading_files_test.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandGetStatusFiles is a function.
|
||||
func TestGitCommandGetStatusFiles(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func([]*models.File)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"No files found",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(files []*models.File) {
|
||||
assert.Len(t, files, 0)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Several files found",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return secureexec.Command(
|
||||
"echo",
|
||||
"MM file1.txt\nA file3.txt\nAM file2.txt\n?? file4.txt\nUU file5.txt",
|
||||
)
|
||||
},
|
||||
func(files []*models.File) {
|
||||
assert.Len(t, files, 5)
|
||||
|
||||
expected := []*models.File{
|
||||
{
|
||||
Name: "file1.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Added: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "MM file1.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "MM",
|
||||
},
|
||||
{
|
||||
Name: "file3.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: false,
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "A file3.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "A ",
|
||||
},
|
||||
{
|
||||
Name: "file2.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "AM file2.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "AM",
|
||||
},
|
||||
{
|
||||
Name: "file4.txt",
|
||||
HasStagedChanges: false,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "?? file4.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "??",
|
||||
},
|
||||
{
|
||||
Name: "file5.txt",
|
||||
HasStagedChanges: false,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Added: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: true,
|
||||
HasInlineMergeConflicts: true,
|
||||
DisplayString: "UU file5.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "UU",
|
||||
},
|
||||
}
|
||||
|
||||
assert.EqualValues(t, expected, files)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
|
||||
s.test(gitCmd.GetStatusFiles(GetStatusFileOptions{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -26,8 +25,7 @@ func (c *GitCommand) GetStashEntries(filterPath string) []*models.StashEntry {
|
||||
return c.getUnfilteredStashEntries()
|
||||
}
|
||||
|
||||
unescaped := fmt.Sprintf("git stash list --name-only")
|
||||
rawString, err := c.OSCommand.RunCommandWithOutput(unescaped)
|
||||
rawString, err := c.RunCommandWithOutput("git stash list --name-only")
|
||||
if err != nil {
|
||||
return c.getUnfilteredStashEntries()
|
||||
}
|
||||
|
||||
61
pkg/commands/loading_stash_test.go
Normal file
61
pkg/commands/loading_stash_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandGetStashEntries is a function.
|
||||
func TestGitCommandGetStashEntries(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func([]*models.StashEntry)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"No stash entries found",
|
||||
func(string, ...string) *exec.Cmd {
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(entries []*models.StashEntry) {
|
||||
assert.Len(t, entries, 0)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Several stash entries found",
|
||||
func(string, ...string) *exec.Cmd {
|
||||
return secureexec.Command("echo", "WIP on add-pkg-commands-test: 55c6af2 increase parallel build\nWIP on master: bb86a3f update github template")
|
||||
},
|
||||
func(entries []*models.StashEntry) {
|
||||
expected := []*models.StashEntry{
|
||||
{
|
||||
Index: 0,
|
||||
Name: "WIP on add-pkg-commands-test: 55c6af2 increase parallel build",
|
||||
},
|
||||
{
|
||||
Index: 1,
|
||||
Name: "WIP on master: bb86a3f update github template",
|
||||
},
|
||||
}
|
||||
|
||||
assert.Len(t, entries, 2)
|
||||
assert.EqualValues(t, expected, entries)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
|
||||
s.test(gitCmd.GetStashEntries(""))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,8 @@ package models
|
||||
|
||||
// CommitFile : A git commit file
|
||||
type CommitFile struct {
|
||||
// Parent is the identifier of the parent object e.g. a commit SHA if this commit file is for a commit, or a stash entry ref like 'stash@{1}'
|
||||
Parent string
|
||||
Name string
|
||||
|
||||
// PatchStatus tells us whether the file has been wholly or partially added to a patch. We might want to pull this logic up into the gui package and make it a map like we do with cherry picked commits
|
||||
PatchStatus int // one of 'WHOLE' 'PART' 'NONE'
|
||||
// TODO: rename this to Path
|
||||
Name string
|
||||
|
||||
ChangeStatus string // e.g. 'A' for added or 'M' for modified. This is based on the result from git diff --name-status
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -10,9 +8,11 @@ import (
|
||||
// duplicating this for now
|
||||
type File struct {
|
||||
Name string
|
||||
PreviousName string
|
||||
HasStagedChanges bool
|
||||
HasUnstagedChanges bool
|
||||
Tracked bool
|
||||
Added bool
|
||||
Deleted bool
|
||||
HasMergeConflicts bool
|
||||
HasInlineMergeConflicts bool
|
||||
@@ -21,15 +21,27 @@ type File struct {
|
||||
ShortStatus string // e.g. 'AD', ' A', 'M ', '??'
|
||||
}
|
||||
|
||||
// sometimes we need to deal with either a node (which contains a file) or an actual file
|
||||
type IFile interface {
|
||||
GetHasUnstagedChanges() bool
|
||||
GetHasStagedChanges() bool
|
||||
GetIsTracked() bool
|
||||
GetPath() string
|
||||
}
|
||||
|
||||
const RENAME_SEPARATOR = " -> "
|
||||
|
||||
func (f *File) IsRename() bool {
|
||||
return strings.Contains(f.Name, RENAME_SEPARATOR)
|
||||
return f.PreviousName != ""
|
||||
}
|
||||
|
||||
// Names returns an array containing just the filename, or in the case of a rename, the after filename and the before filename
|
||||
func (f *File) Names() []string {
|
||||
return strings.Split(f.Name, RENAME_SEPARATOR)
|
||||
result := []string{f.Name}
|
||||
if f.PreviousName != "" {
|
||||
result = append(result, f.PreviousName)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// returns true if the file names are the same or if a a file rename includes the filename of the other
|
||||
@@ -58,3 +70,20 @@ func (f *File) SubmoduleConfig(configs []*SubmoduleConfig) *SubmoduleConfig {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *File) GetHasUnstagedChanges() bool {
|
||||
return f.HasUnstagedChanges
|
||||
}
|
||||
|
||||
func (f *File) GetHasStagedChanges() bool {
|
||||
return f.HasStagedChanges
|
||||
}
|
||||
|
||||
func (f *File) GetIsTracked() bool {
|
||||
return f.Tracked
|
||||
}
|
||||
|
||||
func (f *File) GetPath() string {
|
||||
// TODO: remove concept of name; just use path
|
||||
return f.Name
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
// NOTE: If the return data is empty it won't written anything to stdin
|
||||
func RunCommandWithOutputLiveWrapper(c *OSCommand, command string, output func(string) string) error {
|
||||
c.Log.WithField("command", command).Info("RunCommand")
|
||||
c.LogCommand(command, true)
|
||||
cmd := c.ExecutableFromString(command)
|
||||
cmd.Env = append(cmd.Env, "LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8")
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -16,54 +15,125 @@ import (
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/mgutz/str"
|
||||
"github.com/sirupsen/logrus"
|
||||
gitconfig "github.com/tcnksm/go-gitconfig"
|
||||
)
|
||||
|
||||
// Platform stores the os state
|
||||
type Platform struct {
|
||||
OS string
|
||||
CatCmd string
|
||||
Shell string
|
||||
ShellArg string
|
||||
EscapedQuote string
|
||||
OpenCommand string
|
||||
OpenLinkCommand string
|
||||
FallbackEscapedQuote string
|
||||
OS string
|
||||
CatCmd []string
|
||||
Shell string
|
||||
ShellArg string
|
||||
EscapedQuote string
|
||||
OpenCommand string
|
||||
OpenLinkCommand string
|
||||
}
|
||||
|
||||
// OSCommand holds all the os commands
|
||||
type OSCommand struct {
|
||||
Log *logrus.Entry
|
||||
Platform *Platform
|
||||
Config config.AppConfigurer
|
||||
Command func(string, ...string) *exec.Cmd
|
||||
BeforeExecuteCmd func(*exec.Cmd)
|
||||
GetGlobalGitConfig func(string) (string, error)
|
||||
Getenv func(string) string
|
||||
Log *logrus.Entry
|
||||
Platform *Platform
|
||||
Config config.AppConfigurer
|
||||
Command func(string, ...string) *exec.Cmd
|
||||
BeforeExecuteCmd func(*exec.Cmd)
|
||||
Getenv func(string) string
|
||||
|
||||
// callback to run before running a command, i.e. for the purposes of logging
|
||||
onRunCommand func(CmdLogEntry)
|
||||
|
||||
// something like 'Staging File': allows us to group cmd logs under a single title
|
||||
CmdLogSpan string
|
||||
|
||||
removeFile func(string) error
|
||||
}
|
||||
|
||||
// TODO: make these fields private
|
||||
type CmdLogEntry struct {
|
||||
// e.g. 'git commit -m "haha"'
|
||||
cmdStr string
|
||||
// Span is something like 'Staging File'. Multiple commands can be grouped under the same
|
||||
// span
|
||||
span string
|
||||
|
||||
// sometimes our command is direct like 'git commit', and sometimes it's a
|
||||
// command to remove a file but through Go's standard library rather than the
|
||||
// command line
|
||||
commandLine bool
|
||||
}
|
||||
|
||||
func (e CmdLogEntry) GetCmdStr() string {
|
||||
return e.cmdStr
|
||||
}
|
||||
|
||||
func (e CmdLogEntry) GetSpan() string {
|
||||
return e.span
|
||||
}
|
||||
|
||||
func (e CmdLogEntry) GetCommandLine() bool {
|
||||
return e.commandLine
|
||||
}
|
||||
|
||||
func NewCmdLogEntry(cmdStr string, span string, commandLine bool) CmdLogEntry {
|
||||
return CmdLogEntry{cmdStr: cmdStr, span: span, commandLine: commandLine}
|
||||
}
|
||||
|
||||
// NewOSCommand os command runner
|
||||
func NewOSCommand(log *logrus.Entry, config config.AppConfigurer) *OSCommand {
|
||||
return &OSCommand{
|
||||
Log: log,
|
||||
Platform: getPlatform(),
|
||||
Config: config,
|
||||
Command: exec.Command,
|
||||
BeforeExecuteCmd: func(*exec.Cmd) {},
|
||||
GetGlobalGitConfig: gitconfig.Global,
|
||||
Getenv: os.Getenv,
|
||||
Log: log,
|
||||
Platform: getPlatform(),
|
||||
Config: config,
|
||||
Command: secureexec.Command,
|
||||
BeforeExecuteCmd: func(*exec.Cmd) {},
|
||||
Getenv: os.Getenv,
|
||||
removeFile: os.RemoveAll,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *OSCommand) WithSpan(span string) *OSCommand {
|
||||
// sometimes .WithSpan(span) will be called where span actually is empty, in
|
||||
// which case we don't need to log anything so we can just return early here
|
||||
// with the original struct
|
||||
if span == "" {
|
||||
return c
|
||||
}
|
||||
|
||||
newOSCommand := &OSCommand{}
|
||||
*newOSCommand = *c
|
||||
newOSCommand.CmdLogSpan = span
|
||||
return newOSCommand
|
||||
}
|
||||
|
||||
func (c *OSCommand) LogExecCmd(cmd *exec.Cmd) {
|
||||
c.LogCommand(strings.Join(cmd.Args, " "), true)
|
||||
}
|
||||
|
||||
func (c *OSCommand) LogCommand(cmdStr string, commandLine bool) {
|
||||
c.Log.WithField("command", cmdStr).Info("RunCommand")
|
||||
|
||||
if c.onRunCommand != nil && c.CmdLogSpan != "" {
|
||||
c.onRunCommand(NewCmdLogEntry(cmdStr, c.CmdLogSpan, commandLine))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *OSCommand) SetOnRunCommand(f func(CmdLogEntry)) {
|
||||
c.onRunCommand = f
|
||||
}
|
||||
|
||||
// SetCommand sets the command function used by the struct.
|
||||
// To be used for testing only
|
||||
func (c *OSCommand) SetCommand(cmd func(string, ...string) *exec.Cmd) {
|
||||
c.Command = cmd
|
||||
}
|
||||
|
||||
// To be used for testing only
|
||||
func (c *OSCommand) SetRemoveFile(f func(string) error) {
|
||||
c.removeFile = f
|
||||
}
|
||||
|
||||
func (c *OSCommand) SetBeforeExecuteCmd(cmd func(*exec.Cmd)) {
|
||||
c.BeforeExecuteCmd = cmd
|
||||
}
|
||||
@@ -73,9 +143,12 @@ type RunCommandOptions struct {
|
||||
}
|
||||
|
||||
func (c *OSCommand) RunCommandWithOutputWithOptions(command string, options RunCommandOptions) (string, error) {
|
||||
c.Log.WithField("command", command).Info("RunCommand")
|
||||
c.LogCommand(command, true)
|
||||
cmd := c.ExecutableFromString(command)
|
||||
|
||||
cmd.Env = append(cmd.Env, "GIT_TERMINAL_PROMPT=0") // prevents git from prompting us for input which would freeze the program
|
||||
cmd.Env = append(cmd.Env, options.EnvVars...)
|
||||
|
||||
return sanitisedCommandOutput(cmd.CombinedOutput())
|
||||
}
|
||||
|
||||
@@ -95,17 +168,18 @@ func (c *OSCommand) RunCommandWithOutput(formatString string, formatArgs ...inte
|
||||
if formatArgs != nil {
|
||||
command = fmt.Sprintf(formatString, formatArgs...)
|
||||
}
|
||||
c.Log.WithField("command", command).Info("RunCommand")
|
||||
cmd := c.ExecutableFromString(command)
|
||||
c.LogExecCmd(cmd)
|
||||
output, err := sanitisedCommandOutput(cmd.CombinedOutput())
|
||||
if err != nil {
|
||||
c.Log.WithField("command", command).Error(err)
|
||||
c.Log.WithField("command", command).Error(output)
|
||||
}
|
||||
return output, err
|
||||
}
|
||||
|
||||
// RunExecutableWithOutput runs an executable file and returns its output
|
||||
func (c *OSCommand) RunExecutableWithOutput(cmd *exec.Cmd) (string, error) {
|
||||
c.LogExecCmd(cmd)
|
||||
c.BeforeExecuteCmd(cmd)
|
||||
return sanitisedCommandOutput(cmd.CombinedOutput())
|
||||
}
|
||||
@@ -131,7 +205,7 @@ func (c *OSCommand) ShellCommandFromString(commandStr string) *exec.Cmd {
|
||||
if c.Platform.OS == "windows" {
|
||||
quotedCommand = commandStr
|
||||
} else {
|
||||
quotedCommand = strconv.Quote(commandStr)
|
||||
quotedCommand = c.Quote(commandStr)
|
||||
}
|
||||
|
||||
shellCommand := fmt.Sprintf("%s %s %s", c.Platform.Shell, c.Platform.ShellArg, quotedCommand)
|
||||
@@ -143,6 +217,18 @@ func (c *OSCommand) RunCommandWithOutputLive(command string, output func(string)
|
||||
return RunCommandWithOutputLiveWrapper(c, command, output)
|
||||
}
|
||||
|
||||
func (c *OSCommand) CatFile(filename string) (string, error) {
|
||||
arr := append(c.Platform.CatCmd, filename)
|
||||
cmdStr := strings.Join(arr, " ")
|
||||
c.Log.WithField("command", cmdStr).Info("Cat")
|
||||
cmd := c.Command(arr[0], arr[1:]...)
|
||||
output, err := sanitisedCommandOutput(cmd.CombinedOutput())
|
||||
if err != nil {
|
||||
c.Log.WithField("command", cmdStr).Error(output)
|
||||
}
|
||||
return output, err
|
||||
}
|
||||
|
||||
// DetectUnamePass detect a username / password / passphrase question in a command
|
||||
// promptUserForCredential is a function that gets executed when this function detect you need to fillin a password or passphrase
|
||||
// The promptUserForCredential argument will be "username", "password" or "passphrase" and expects the user's password/passphrase or username back
|
||||
@@ -176,6 +262,17 @@ func (c *OSCommand) RunCommand(formatString string, formatArgs ...interface{}) e
|
||||
return err
|
||||
}
|
||||
|
||||
// RunShellCommand runs shell commands i.e. 'sh -c <command>'. Good for when you
|
||||
// need access to the shell
|
||||
func (c *OSCommand) RunShellCommand(command string) error {
|
||||
cmd := c.Command(c.Platform.Shell, c.Platform.ShellArg, command)
|
||||
c.LogExecCmd(cmd)
|
||||
|
||||
_, err := sanitisedCommandOutput(cmd.CombinedOutput())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// FileType tells us if the file is a file, directory or other
|
||||
func (c *OSCommand) FileType(path string) string {
|
||||
fileInfo, err := os.Stat(path)
|
||||
@@ -188,16 +285,6 @@ func (c *OSCommand) FileType(path string) string {
|
||||
return "file"
|
||||
}
|
||||
|
||||
// RunDirectCommand wrapper around direct commands
|
||||
func (c *OSCommand) RunDirectCommand(command string) (string, error) {
|
||||
c.Log.WithField("command", command).Info("RunDirectCommand")
|
||||
|
||||
return sanitisedCommandOutput(
|
||||
c.Command(c.Platform.Shell, c.Platform.ShellArg, command).
|
||||
CombinedOutput(),
|
||||
)
|
||||
}
|
||||
|
||||
func sanitisedCommandOutput(output []byte, err error) (string, error) {
|
||||
outputString := string(output)
|
||||
if err != nil {
|
||||
@@ -225,6 +312,7 @@ func (c *OSCommand) OpenFile(filename string) error {
|
||||
|
||||
// OpenLink opens a file with the given
|
||||
func (c *OSCommand) OpenLink(link string) error {
|
||||
c.LogCommand(fmt.Sprintf("Opening link '%s'", link), false)
|
||||
commandTemplate := c.Config.GetUserConfig().OS.OpenLinkCommand
|
||||
templateValues := map[string]string{
|
||||
"link": c.Quote(link),
|
||||
@@ -235,31 +323,6 @@ func (c *OSCommand) OpenLink(link string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// EditFile opens a file in a subprocess using whatever editor is available,
|
||||
// falling back to core.editor, VISUAL, EDITOR, then vi
|
||||
func (c *OSCommand) EditFile(filename string) (*exec.Cmd, error) {
|
||||
editor, _ := c.GetGlobalGitConfig("core.editor")
|
||||
|
||||
if editor == "" {
|
||||
editor = c.Getenv("VISUAL")
|
||||
}
|
||||
if editor == "" {
|
||||
editor = c.Getenv("EDITOR")
|
||||
}
|
||||
if editor == "" {
|
||||
if err := c.RunCommand("which vi"); err == nil {
|
||||
editor = "vi"
|
||||
}
|
||||
}
|
||||
if editor == "" {
|
||||
return nil, errors.New("No editor defined in $VISUAL, $EDITOR, or git config")
|
||||
}
|
||||
|
||||
splitCmd := str.ToArgv(fmt.Sprintf("%s %s", editor, c.Quote(filename)))
|
||||
|
||||
return c.PrepareSubProcess(splitCmd[0], splitCmd[1:]...), nil
|
||||
}
|
||||
|
||||
// PrepareSubProcess iniPrepareSubProcessrocess then tells the Gui to switch to it
|
||||
// TODO: see if this needs to exist, given that ExecutableFromString does the same things
|
||||
func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *exec.Cmd {
|
||||
@@ -267,27 +330,33 @@ func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *ex
|
||||
if cmd != nil {
|
||||
cmd.Env = append(os.Environ(), "GIT_OPTIONAL_LOCKS=0")
|
||||
}
|
||||
c.LogExecCmd(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// PrepareShellSubProcess returns the pointer to a custom command
|
||||
func (c *OSCommand) PrepareShellSubProcess(command string) *exec.Cmd {
|
||||
return c.PrepareSubProcess(c.Platform.Shell, c.Platform.ShellArg, command)
|
||||
}
|
||||
|
||||
// Quote wraps a message in platform-specific quotation marks
|
||||
func (c *OSCommand) Quote(message string) string {
|
||||
message = strings.Replace(message, "`", "\\`", -1)
|
||||
escapedQuote := c.Platform.EscapedQuote
|
||||
if strings.Contains(message, c.Platform.EscapedQuote) {
|
||||
escapedQuote = c.Platform.FallbackEscapedQuote
|
||||
if c.Platform.OS == "windows" {
|
||||
message = strings.Replace(message, `"`, `"'"'"`, -1)
|
||||
message = strings.Replace(message, `\"`, `\\"`, -1)
|
||||
} else {
|
||||
message = strings.Replace(message, `\`, `\\`, -1)
|
||||
message = strings.Replace(message, `"`, `\"`, -1)
|
||||
message = strings.Replace(message, "`", "\\`", -1)
|
||||
message = strings.Replace(message, "$", "\\$", -1)
|
||||
}
|
||||
escapedQuote := c.Platform.EscapedQuote
|
||||
return escapedQuote + message + escapedQuote
|
||||
}
|
||||
|
||||
// Unquote removes wrapping quotations marks if they are present
|
||||
// this is needed for removing quotes from staged filenames with spaces
|
||||
func (c *OSCommand) Unquote(message string) string {
|
||||
return strings.Replace(message, `"`, "", -1)
|
||||
}
|
||||
|
||||
// AppendLineToFile adds a new line in file
|
||||
func (c *OSCommand) AppendLineToFile(filename, line string) error {
|
||||
c.LogCommand(fmt.Sprintf("Appending '%s' to file '%s'", line, filename), false)
|
||||
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
return utils.WrapError(err)
|
||||
@@ -308,6 +377,7 @@ func (c *OSCommand) CreateTempFile(filename, content string) (string, error) {
|
||||
c.Log.Error(err)
|
||||
return "", utils.WrapError(err)
|
||||
}
|
||||
c.LogCommand(fmt.Sprintf("Creating temp file '%s'", tmpfile.Name()), false)
|
||||
|
||||
if _, err := tmpfile.WriteString(content); err != nil {
|
||||
c.Log.Error(err)
|
||||
@@ -323,6 +393,7 @@ func (c *OSCommand) CreateTempFile(filename, content string) (string, error) {
|
||||
|
||||
// CreateFileWithContent creates a file with the given content
|
||||
func (c *OSCommand) CreateFileWithContent(path string, content string) error {
|
||||
c.LogCommand(fmt.Sprintf("Creating file '%s'", path), false)
|
||||
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
|
||||
c.Log.Error(err)
|
||||
return err
|
||||
@@ -338,6 +409,7 @@ func (c *OSCommand) CreateFileWithContent(path string, content string) error {
|
||||
|
||||
// Remove removes a file or directory at the specified path
|
||||
func (c *OSCommand) Remove(filename string) error {
|
||||
c.LogCommand(fmt.Sprintf("Removing '%s'", filename), false)
|
||||
err := os.RemoveAll(filename)
|
||||
return utils.WrapError(err)
|
||||
}
|
||||
@@ -358,6 +430,7 @@ func (c *OSCommand) FileExists(path string) (bool, error) {
|
||||
// before running it
|
||||
func (c *OSCommand) RunPreparedCommand(cmd *exec.Cmd) error {
|
||||
c.BeforeExecuteCmd(cmd)
|
||||
c.LogExecCmd(cmd)
|
||||
out, err := cmd.CombinedOutput()
|
||||
outString := string(out)
|
||||
c.Log.Info(outString)
|
||||
@@ -379,19 +452,18 @@ func (c *OSCommand) GetLazygitPath() string {
|
||||
return `"` + filepath.ToSlash(ex) + `"`
|
||||
}
|
||||
|
||||
// RunCustomCommand returns the pointer to a custom command
|
||||
func (c *OSCommand) RunCustomCommand(command string) *exec.Cmd {
|
||||
return c.PrepareSubProcess(c.Platform.Shell, c.Platform.ShellArg, command)
|
||||
}
|
||||
|
||||
// PipeCommands runs a heap of commands and pipes their inputs/outputs together like A | B | C
|
||||
func (c *OSCommand) PipeCommands(commandStrings ...string) error {
|
||||
|
||||
cmds := make([]*exec.Cmd, len(commandStrings))
|
||||
|
||||
logCmdStr := ""
|
||||
for i, str := range commandStrings {
|
||||
if i > 0 {
|
||||
logCmdStr += " | "
|
||||
}
|
||||
logCmdStr += str
|
||||
cmds[i] = c.ExecutableFromString(str)
|
||||
}
|
||||
c.LogCommand(logCmdStr, true)
|
||||
|
||||
for i := 0; i < len(cmds)-1; i++ {
|
||||
stdout, err := cmds[i].StdoutPipe()
|
||||
@@ -471,15 +543,23 @@ func RunLineOutputCmd(cmd *exec.Cmd, onLine func(line string) (bool, error)) err
|
||||
return err
|
||||
}
|
||||
if stop {
|
||||
cmd.Process.Kill()
|
||||
_ = cmd.Process.Kill()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cmd.Wait()
|
||||
_ = cmd.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *OSCommand) CopyToClipboard(str string) error {
|
||||
c.LogCommand(fmt.Sprintf("Copying '%s' to clipboard", utils.TruncateWithEllipsis(str, 40)), false)
|
||||
return clipboard.WriteAll(str)
|
||||
}
|
||||
|
||||
func (c *OSCommand) RemoveFile(path string) error {
|
||||
c.LogCommand(fmt.Sprintf("Deleting path '%s'", path), false)
|
||||
|
||||
return c.removeFile(path)
|
||||
}
|
||||
|
||||
@@ -8,13 +8,12 @@ import (
|
||||
|
||||
func getPlatform() *Platform {
|
||||
return &Platform{
|
||||
OS: runtime.GOOS,
|
||||
CatCmd: "cat",
|
||||
Shell: "bash",
|
||||
ShellArg: "-c",
|
||||
EscapedQuote: "'",
|
||||
OpenCommand: "open {{filename}}",
|
||||
OpenLinkCommand: "open {{link}}",
|
||||
FallbackEscapedQuote: "\"",
|
||||
OS: runtime.GOOS,
|
||||
CatCmd: []string{"cat"},
|
||||
Shell: "bash",
|
||||
ShellArg: "-c",
|
||||
EscapedQuote: `"`,
|
||||
OpenCommand: "open {{filename}}",
|
||||
OpenLinkCommand: "open {{link}}",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -70,7 +71,7 @@ func TestOSCommandOpenFile(t *testing.T) {
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
return exec.Command("exit", "1")
|
||||
return secureexec.Command("exit", "1")
|
||||
},
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
@@ -81,7 +82,7 @@ func TestOSCommandOpenFile(t *testing.T) {
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "open", name)
|
||||
assert.Equal(t, []string{"test"}, arg)
|
||||
return exec.Command("echo")
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
@@ -92,7 +93,7 @@ func TestOSCommandOpenFile(t *testing.T) {
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "open", name)
|
||||
assert.Equal(t, []string{"filename with spaces"}, arg)
|
||||
return exec.Command("echo")
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
@@ -109,162 +110,12 @@ func TestOSCommandOpenFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestOSCommandEditFile is a function.
|
||||
func TestOSCommandEditFile(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
getenv func(string) string
|
||||
getGlobalGitConfig func(string) (string, error)
|
||||
test func(*exec.Cmd, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
return exec.Command("exit", "1")
|
||||
},
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmd *exec.Cmd, err error) {
|
||||
assert.EqualError(t, err, "No editor defined in $VISUAL, $EDITOR, or git config")
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
if name == "which" {
|
||||
return exec.Command("exit", "1")
|
||||
}
|
||||
|
||||
assert.EqualValues(t, "nano", name)
|
||||
|
||||
return nil
|
||||
},
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "nano", nil
|
||||
},
|
||||
func(cmd *exec.Cmd, err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
if name == "which" {
|
||||
return exec.Command("exit", "1")
|
||||
}
|
||||
|
||||
assert.EqualValues(t, "nano", name)
|
||||
|
||||
return nil
|
||||
},
|
||||
func(env string) string {
|
||||
if env == "VISUAL" {
|
||||
return "nano"
|
||||
}
|
||||
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmd *exec.Cmd, err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
if name == "which" {
|
||||
return exec.Command("exit", "1")
|
||||
}
|
||||
|
||||
assert.EqualValues(t, "emacs", name)
|
||||
|
||||
return nil
|
||||
},
|
||||
func(env string) string {
|
||||
if env == "EDITOR" {
|
||||
return "emacs"
|
||||
}
|
||||
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmd *exec.Cmd, err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
if name == "which" {
|
||||
return exec.Command("echo")
|
||||
}
|
||||
|
||||
assert.EqualValues(t, "vi", name)
|
||||
|
||||
return nil
|
||||
},
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmd *exec.Cmd, err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"file/with space",
|
||||
func(name string, args ...string) *exec.Cmd {
|
||||
if name == "which" {
|
||||
return exec.Command("echo")
|
||||
}
|
||||
|
||||
assert.EqualValues(t, "vi", name)
|
||||
assert.EqualValues(t, "file/with space", args[0])
|
||||
|
||||
return nil
|
||||
},
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmd *exec.Cmd, err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
OSCmd := NewDummyOSCommand()
|
||||
OSCmd.Command = s.command
|
||||
OSCmd.GetGlobalGitConfig = s.getGlobalGitConfig
|
||||
OSCmd.Getenv = s.getenv
|
||||
|
||||
s.test(OSCmd.EditFile(s.filename))
|
||||
}
|
||||
}
|
||||
|
||||
// TestOSCommandQuote is a function.
|
||||
func TestOSCommandQuote(t *testing.T) {
|
||||
osCommand := NewDummyOSCommand()
|
||||
|
||||
osCommand.Platform.OS = "linux"
|
||||
|
||||
actual := osCommand.Quote("hello `test`")
|
||||
|
||||
expected := osCommand.Platform.EscapedQuote + "hello \\`test\\`" + osCommand.Platform.EscapedQuote
|
||||
@@ -280,7 +131,7 @@ func TestOSCommandQuoteSingleQuote(t *testing.T) {
|
||||
|
||||
actual := osCommand.Quote("hello 'test'")
|
||||
|
||||
expected := osCommand.Platform.FallbackEscapedQuote + "hello 'test'" + osCommand.Platform.FallbackEscapedQuote
|
||||
expected := osCommand.Platform.EscapedQuote + "hello 'test'" + osCommand.Platform.EscapedQuote
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
@@ -293,18 +144,20 @@ func TestOSCommandQuoteDoubleQuote(t *testing.T) {
|
||||
|
||||
actual := osCommand.Quote(`hello "test"`)
|
||||
|
||||
expected := osCommand.Platform.EscapedQuote + "hello \"test\"" + osCommand.Platform.EscapedQuote
|
||||
expected := osCommand.Platform.EscapedQuote + `hello \"test\"` + osCommand.Platform.EscapedQuote
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
|
||||
// TestOSCommandUnquote is a function.
|
||||
func TestOSCommandUnquote(t *testing.T) {
|
||||
// TestOSCommandQuoteWindows tests the quote function for Windows
|
||||
func TestOSCommandQuoteWindows(t *testing.T) {
|
||||
osCommand := NewDummyOSCommand()
|
||||
|
||||
actual := osCommand.Unquote(`hello "test"`)
|
||||
osCommand.Platform.OS = "windows"
|
||||
|
||||
expected := "hello test"
|
||||
actual := osCommand.Quote(`hello "test"`)
|
||||
|
||||
expected := osCommand.Platform.EscapedQuote + `hello "'"'"test"'"'"` + osCommand.Platform.EscapedQuote
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@ package oscommands
|
||||
|
||||
func getPlatform() *Platform {
|
||||
return &Platform{
|
||||
OS: "windows",
|
||||
CatCmd: "type",
|
||||
Shell: "cmd",
|
||||
ShellArg: "/c",
|
||||
EscapedQuote: `\"`,
|
||||
FallbackEscapedQuote: "\\'",
|
||||
OS: "windows",
|
||||
CatCmd: []string{"cmd", "/c", "type"},
|
||||
Shell: "cmd",
|
||||
ShellArg: "/c",
|
||||
EscapedQuote: `\"`,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -9,9 +8,11 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type PatchStatus int
|
||||
|
||||
const (
|
||||
// UNSELECTED is for when the commit file has not been added to the patch in any way
|
||||
UNSELECTED = iota
|
||||
UNSELECTED PatchStatus = 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
|
||||
@@ -20,7 +21,7 @@ const (
|
||||
)
|
||||
|
||||
type fileInfo struct {
|
||||
mode int // one of WHOLE/PART
|
||||
mode PatchStatus
|
||||
includedLineIndices []int
|
||||
diff string
|
||||
}
|
||||
@@ -81,20 +82,25 @@ func (p *PatchManager) removeFile(info *fileInfo) {
|
||||
info.includedLineIndices = nil
|
||||
}
|
||||
|
||||
func (p *PatchManager) ToggleFileWhole(filename string) error {
|
||||
func (p *PatchManager) AddFileWhole(filename string) error {
|
||||
info, err := p.getFileInfo(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch info.mode {
|
||||
case UNSELECTED, PART:
|
||||
p.addFileWhole(info)
|
||||
case WHOLE:
|
||||
p.removeFile(info)
|
||||
default:
|
||||
return errors.New("unknown file mode")
|
||||
|
||||
p.addFileWhole(info)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PatchManager) RemoveFile(filename string) error {
|
||||
info, err := p.getFileInfo(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.removeFile(info)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -176,11 +182,8 @@ func (p *PatchManager) RenderPatchForFile(filename string, plain bool, reverse b
|
||||
if plain {
|
||||
return patch
|
||||
}
|
||||
parser, err := NewPatchParser(p.Log, patch)
|
||||
if err != nil {
|
||||
// swallowing for now
|
||||
return ""
|
||||
}
|
||||
parser := NewPatchParser(p.Log, patch)
|
||||
|
||||
// not passing included lines because we don't want to see them in the secondary panel
|
||||
return parser.Render(-1, -1, nil)
|
||||
}
|
||||
@@ -216,7 +219,11 @@ func (p *PatchManager) RenderAggregatedPatchColored(plain bool) string {
|
||||
return result
|
||||
}
|
||||
|
||||
func (p *PatchManager) GetFileStatus(filename string) int {
|
||||
func (p *PatchManager) GetFileStatus(filename string, parent string) PatchStatus {
|
||||
if parent != p.To {
|
||||
return UNSELECTED
|
||||
}
|
||||
|
||||
info, ok := p.fileInfoMap[filename]
|
||||
if !ok {
|
||||
return UNSELECTED
|
||||
|
||||
@@ -10,8 +10,10 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type PatchLineKind int
|
||||
|
||||
const (
|
||||
PATCH_HEADER = iota
|
||||
PATCH_HEADER PatchLineKind = iota
|
||||
COMMIT_SHA
|
||||
COMMIT_DESCRIPTION
|
||||
HUNK_HEADER
|
||||
@@ -24,7 +26,7 @@ const (
|
||||
// the job of this file is to parse a diff, find out where the hunks begin and end, which lines are stageable, and how to find the next hunk from the current position or the next stageable line from the current position.
|
||||
|
||||
type PatchLine struct {
|
||||
Kind int
|
||||
Kind PatchLineKind
|
||||
Content string // something like '+ hello' (note the first character is not removed)
|
||||
}
|
||||
|
||||
@@ -37,11 +39,8 @@ type PatchParser struct {
|
||||
}
|
||||
|
||||
// NewPatchParser builds a new branch list builder
|
||||
func NewPatchParser(log *logrus.Entry, patch string) (*PatchParser, error) {
|
||||
hunkStarts, stageableLines, patchLines, err := parsePatch(patch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func NewPatchParser(log *logrus.Entry, patch string) *PatchParser {
|
||||
hunkStarts, stageableLines, patchLines := parsePatch(patch)
|
||||
|
||||
patchHunks := GetHunksFromDiff(patch)
|
||||
|
||||
@@ -51,7 +50,7 @@ func NewPatchParser(log *logrus.Entry, patch string) (*PatchParser, error) {
|
||||
StageableLines: stageableLines,
|
||||
PatchLines: patchLines,
|
||||
PatchHunks: patchHunks,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// GetHunkContainingLine takes a line index and an offset and finds the hunk
|
||||
@@ -137,14 +136,14 @@ func coloredString(colorAttr color.Attribute, str string, selected bool, include
|
||||
return utils.ColoredStringDirect(str[:1], clIncluded) + utils.ColoredStringDirect(str[1:], cl)
|
||||
}
|
||||
|
||||
func parsePatch(patch string) ([]int, []int, []*PatchLine, error) {
|
||||
func parsePatch(patch string) ([]int, []int, []*PatchLine) {
|
||||
lines := strings.Split(patch, "\n")
|
||||
hunkStarts := []int{}
|
||||
stageableLines := []int{}
|
||||
pastFirstHunkHeader := false
|
||||
pastCommitDescription := true
|
||||
patchLines := make([]*PatchLine, len(lines))
|
||||
var lineKind int
|
||||
var lineKind PatchLineKind
|
||||
var firstChar string
|
||||
for index, line := range lines {
|
||||
firstChar = " "
|
||||
@@ -183,7 +182,7 @@ func parsePatch(patch string) ([]int, []int, []*PatchLine, error) {
|
||||
}
|
||||
patchLines[index] = &PatchLine{Kind: lineKind, Content: line}
|
||||
}
|
||||
return hunkStarts, stageableLines, patchLines, nil
|
||||
return hunkStarts, stageableLines, patchLines
|
||||
}
|
||||
|
||||
// Render returns the coloured string of the diff with any selected lines highlighted
|
||||
|
||||
@@ -23,7 +23,7 @@ func (c *GitCommand) DeletePatchesFromCommit(commits []*models.Commit, commitInd
|
||||
}
|
||||
|
||||
// time to amend the selected commit
|
||||
if _, err := c.AmendHead(); err != nil {
|
||||
if err := c.AmendHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
|
||||
}
|
||||
|
||||
// amend the destination commit
|
||||
if _, err := c.AmendHead(); err != nil {
|
||||
if err := c.AmendHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
|
||||
// we can make this GPG thing possible it just means we need to do this in two parts:
|
||||
// one where we handle the possibility of a credential request, and the other
|
||||
// where we continue the rebase
|
||||
if c.usingGpg() {
|
||||
if c.UsingGpg() {
|
||||
return errors.New(c.Tr.DisabledForGPG)
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
|
||||
}
|
||||
|
||||
// amend the source commit
|
||||
if _, err := c.AmendHead(); err != nil {
|
||||
if err := c.AmendHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
|
||||
}
|
||||
|
||||
// amend the destination commit
|
||||
if _, err := c.AmendHead(); err != nil {
|
||||
if err := c.AmendHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
|
||||
return c.GenericMergeOrRebaseAction("rebase", "continue")
|
||||
}
|
||||
|
||||
func (c *GitCommand) PullPatchIntoIndex(commits []*models.Commit, commitIdx int, p *patch.PatchManager, stash bool) error {
|
||||
func (c *GitCommand) MovePatchIntoIndex(commits []*models.Commit, commitIdx int, p *patch.PatchManager, stash bool) error {
|
||||
if stash {
|
||||
if err := c.StashSave(c.Tr.StashPrefix + commits[commitIdx].Sha); err != nil {
|
||||
return err
|
||||
@@ -149,7 +149,7 @@ func (c *GitCommand) PullPatchIntoIndex(commits []*models.Commit, commitIdx int,
|
||||
}
|
||||
|
||||
if err := p.ApplyPatches(true); err != nil {
|
||||
if c.WorkingTreeState() == "rebasing" {
|
||||
if c.WorkingTreeState() == REBASE_MODE_REBASING {
|
||||
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -158,7 +158,7 @@ func (c *GitCommand) PullPatchIntoIndex(commits []*models.Commit, commitIdx int,
|
||||
}
|
||||
|
||||
// amend the commit
|
||||
if _, err := c.AmendHead(); err != nil {
|
||||
if err := c.AmendHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ func (c *GitCommand) PullPatchIntoIndex(commits []*models.Commit, commitIdx int,
|
||||
c.onSuccessfulContinue = func() error {
|
||||
// add patches to index
|
||||
if err := p.ApplyPatches(false); err != nil {
|
||||
if c.WorkingTreeState() == "rebasing" {
|
||||
if c.WorkingTreeState() == REBASE_MODE_REBASING {
|
||||
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -203,7 +203,7 @@ func (c *GitCommand) PullPatchIntoNewCommit(commits []*models.Commit, commitIdx
|
||||
}
|
||||
|
||||
// amend the commit
|
||||
if _, err := c.AmendHead(); err != nil {
|
||||
if err := c.AmendHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ func (c *GitCommand) PullPatchIntoNewCommit(commits []*models.Commit, commitIdx
|
||||
|
||||
head_message, _ := c.GetHeadCommitMessage()
|
||||
new_message := fmt.Sprintf("Split from \"%s\"", head_message)
|
||||
_, err := c.Commit(new_message, "")
|
||||
err := c.OSCommand.RunCommand(c.CommitCmdStr(new_message, ""))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -90,11 +90,30 @@ func NewPullRequest(gitCommand *GitCommand) *PullRequest {
|
||||
}
|
||||
|
||||
// Create opens link to new pull request in browser
|
||||
func (pr *PullRequest) Create(branch *models.Branch) error {
|
||||
func (pr *PullRequest) Create(branch *models.Branch) (string, error) {
|
||||
pullRequestURL, err := pr.getPullRequestURL(branch)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return pullRequestURL, pr.GitCommand.OSCommand.OpenLink(pullRequestURL)
|
||||
}
|
||||
|
||||
// CopyURL copies the pull request URL to the clipboard
|
||||
func (pr *PullRequest) CopyURL(branch *models.Branch) (string, error) {
|
||||
pullRequestURL, err := pr.getPullRequestURL(branch)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return pullRequestURL, pr.GitCommand.OSCommand.CopyToClipboard(pullRequestURL)
|
||||
}
|
||||
|
||||
func (pr *PullRequest) getPullRequestURL(branch *models.Branch) (string, error) {
|
||||
branchExistsOnRemote := pr.GitCommand.CheckRemoteBranchExists(branch)
|
||||
|
||||
if !branchExistsOnRemote {
|
||||
return errors.New(pr.GitCommand.Tr.NoBranchOnRemote)
|
||||
return "", errors.New(pr.GitCommand.Tr.NoBranchOnRemote)
|
||||
}
|
||||
|
||||
repoURL := pr.GitCommand.GetRemoteURL()
|
||||
@@ -108,14 +127,15 @@ func (pr *PullRequest) Create(branch *models.Branch) error {
|
||||
}
|
||||
|
||||
if gitService == nil {
|
||||
return errors.New(pr.GitCommand.Tr.UnsupportedGitService)
|
||||
return "", errors.New(pr.GitCommand.Tr.UnsupportedGitService)
|
||||
}
|
||||
|
||||
repoInfo := getRepoInfoFromURL(repoURL)
|
||||
|
||||
return pr.GitCommand.OSCommand.OpenLink(fmt.Sprintf(
|
||||
pullRequestURL := fmt.Sprintf(
|
||||
gitService.PullRequestURL, repoInfo.Owner, repoInfo.Repository, branch.Name,
|
||||
))
|
||||
)
|
||||
|
||||
return pullRequestURL, nil
|
||||
}
|
||||
|
||||
func getRepoInfoFromURL(url string) *RepoInformation {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -46,98 +47,108 @@ func TestGetRepoInfoFromURL(t *testing.T) {
|
||||
// TestCreatePullRequest is a function.
|
||||
func TestCreatePullRequest(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
branch *models.Branch
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(err error)
|
||||
testName string
|
||||
branch *models.Branch
|
||||
remoteUrl string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(url string, err error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Opens a link to new pull request on bitbucket",
|
||||
&models.Branch{
|
||||
testName: "Opens a link to new pull request on bitbucket",
|
||||
branch: &models.Branch{
|
||||
Name: "feature/profile-page",
|
||||
},
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return exec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
|
||||
return secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "open")
|
||||
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1"})
|
||||
return exec.Command("echo")
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Opens a link to new pull request on bitbucket with http remote url",
|
||||
&models.Branch{
|
||||
testName: "Opens a link to new pull request on bitbucket with http remote url",
|
||||
branch: &models.Branch{
|
||||
Name: "feature/events",
|
||||
},
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return exec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
|
||||
return secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "open")
|
||||
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1"})
|
||||
return exec.Command("echo")
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Opens a link to new pull request on github",
|
||||
&models.Branch{
|
||||
testName: "Opens a link to new pull request on github",
|
||||
branch: &models.Branch{
|
||||
Name: "feature/sum-operation",
|
||||
},
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
remoteUrl: "git@github.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return exec.Command("echo", "git@github.com:peter/calculator.git")
|
||||
return secureexec.Command("echo", "git@github.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "open")
|
||||
assert.Equal(t, args, []string{"https://github.com/peter/calculator/compare/feature/sum-operation?expand=1"})
|
||||
return exec.Command("echo")
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Opens a link to new pull request on gitlab",
|
||||
&models.Branch{
|
||||
testName: "Opens a link to new pull request on gitlab",
|
||||
branch: &models.Branch{
|
||||
Name: "feature/ui",
|
||||
},
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
remoteUrl: "git@gitlab.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return exec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "open")
|
||||
assert.Equal(t, args, []string{"https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"})
|
||||
return exec.Command("echo")
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Throws an error if git service is unsupported",
|
||||
&models.Branch{
|
||||
testName: "Throws an error if git service is unsupported",
|
||||
branch: &models.Branch{
|
||||
Name: "feature/divide-operation",
|
||||
},
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return exec.Command("echo", "git@something.com:peter/calculator.git")
|
||||
remoteUrl: "git@something.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
test: func(url string, err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
@@ -155,6 +166,10 @@ func TestCreatePullRequest(t *testing.T) {
|
||||
"invalid.work.com": "noservice:invalid.work.com",
|
||||
"noservice.work.com": "noservice.work.com",
|
||||
}
|
||||
gitCommand.getGitConfigValue = func(path string) (string, error) {
|
||||
assert.Equal(t, path, "remote.origin.url")
|
||||
return s.remoteUrl, nil
|
||||
}
|
||||
dummyPullRequest := NewPullRequest(gitCommand)
|
||||
s.test(dummyPullRequest.Create(s.branch))
|
||||
})
|
||||
|
||||
@@ -77,6 +77,8 @@ func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string
|
||||
gitSequenceEditor := ex
|
||||
if todo == "" {
|
||||
gitSequenceEditor = "true"
|
||||
} else {
|
||||
c.OSCommand.LogCommand(fmt.Sprintf("Creating TODO file for interactive rebase: \n\n%s", todo), false)
|
||||
}
|
||||
|
||||
cmd.Env = os.Environ()
|
||||
@@ -211,7 +213,7 @@ func (c *GitCommand) BeginInteractiveRebaseForCommit(commits []*models.Commit, c
|
||||
// we can make this GPG thing possible it just means we need to do this in two parts:
|
||||
// one where we handle the possibility of a credential request, and the other
|
||||
// where we continue the rebase
|
||||
if c.usingGpg() {
|
||||
if c.UsingGpg() {
|
||||
return errors.New(c.Tr.DisabledForGPG)
|
||||
}
|
||||
|
||||
|
||||
96
pkg/commands/rebasing_test.go
Normal file
96
pkg/commands/rebasing_test.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandRebaseBranch is a function.
|
||||
func TestGitCommandRebaseBranch(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
arg string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"successful rebase",
|
||||
"master",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git rebase --interactive --autostash --keep-empty master",
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"unsuccessful rebase",
|
||||
"master",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git rebase --interactive --autostash --keep-empty master",
|
||||
Replace: "test",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.RebaseBranch(s.arg))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandSkipEditorCommand confirms that SkipEditorCommand injects
|
||||
// environment variables that suppress an interactive editor
|
||||
func TestGitCommandSkipEditorCommand(t *testing.T) {
|
||||
cmd := NewDummyGitCommand()
|
||||
|
||||
cmd.OSCommand.SetBeforeExecuteCmd(func(cmd *exec.Cmd) {
|
||||
test.AssertContainsMatch(
|
||||
t,
|
||||
cmd.Env,
|
||||
regexp.MustCompile("^VISUAL="),
|
||||
"expected VISUAL to be set for a non-interactive external command",
|
||||
)
|
||||
|
||||
test.AssertContainsMatch(
|
||||
t,
|
||||
cmd.Env,
|
||||
regexp.MustCompile("^EDITOR="),
|
||||
"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,
|
||||
regexp.MustCompile("^LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY$"),
|
||||
"expected LAZYGIT_CLIENT_COMMAND to be set for a non-interactive external command",
|
||||
)
|
||||
})
|
||||
|
||||
_ = cmd.runSkipEditorCommand("true")
|
||||
}
|
||||
@@ -7,19 +7,19 @@ import (
|
||||
)
|
||||
|
||||
func (c *GitCommand) AddRemote(name string, url string) error {
|
||||
return c.OSCommand.RunCommand("git remote add %s %s", name, url)
|
||||
return c.RunCommand("git remote add %s %s", name, url)
|
||||
}
|
||||
|
||||
func (c *GitCommand) RemoveRemote(name string) error {
|
||||
return c.OSCommand.RunCommand("git remote remove %s", name)
|
||||
return c.RunCommand("git remote remove %s", name)
|
||||
}
|
||||
|
||||
func (c *GitCommand) RenameRemote(oldRemoteName string, newRemoteName string) error {
|
||||
return c.OSCommand.RunCommand("git remote rename %s %s", oldRemoteName, newRemoteName)
|
||||
return c.RunCommand("git remote rename %s %s", oldRemoteName, newRemoteName)
|
||||
}
|
||||
|
||||
func (c *GitCommand) UpdateRemoteUrl(remoteName string, updatedUrl string) error {
|
||||
return c.OSCommand.RunCommand("git remote set-url %s %s", remoteName, updatedUrl)
|
||||
return c.RunCommand("git remote set-url %s %s", remoteName, updatedUrl)
|
||||
}
|
||||
|
||||
func (c *GitCommand) DeleteRemoteBranch(remoteName string, branchName string, promptUserForCredential func(string) string) error {
|
||||
|
||||
@@ -4,13 +4,13 @@ import "fmt"
|
||||
|
||||
// StashDo modify stash
|
||||
func (c *GitCommand) StashDo(index int, method string) error {
|
||||
return c.OSCommand.RunCommand("git stash %s stash@{%d}", method, index)
|
||||
return c.RunCommand("git stash %s stash@{%d}", method, index)
|
||||
}
|
||||
|
||||
// StashSave save stash
|
||||
// TODO: before calling this, check if there is anything to save
|
||||
func (c *GitCommand) StashSave(message string) error {
|
||||
return c.OSCommand.RunCommand("git stash save %s", c.OSCommand.Quote(message))
|
||||
return c.RunCommand("git stash save %s", c.OSCommand.Quote(message))
|
||||
}
|
||||
|
||||
// GetStashEntryDiff stash diff
|
||||
@@ -21,8 +21,8 @@ func (c *GitCommand) ShowStashEntryCmdStr(index int) string {
|
||||
// StashSaveStagedChanges stashes only the currently staged changes. This takes a few steps
|
||||
// shoutouts to Joe on https://stackoverflow.com/questions/14759748/stashing-only-staged-changes-in-git-is-it-possible
|
||||
func (c *GitCommand) StashSaveStagedChanges(message string) error {
|
||||
|
||||
if err := c.OSCommand.RunCommand("git stash --keep-index"); err != nil {
|
||||
// wrap in 'writing', which uses a mutex
|
||||
if err := c.RunCommand("git stash --keep-index"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ func (c *GitCommand) StashSaveStagedChanges(message string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.OSCommand.RunCommand("git stash apply stash@{1}"); err != nil {
|
||||
if err := c.RunCommand("git stash apply stash@{1}"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ func (c *GitCommand) StashSaveStagedChanges(message string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.OSCommand.RunCommand("git stash drop stash@{1}"); err != nil {
|
||||
if err := c.RunCommand("git stash drop stash@{1}"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ func (c *GitCommand) StashSaveStagedChanges(message string) error {
|
||||
files := c.GetStatusFiles(GetStatusFileOptions{})
|
||||
for _, file := range files {
|
||||
if file.ShortStatus == "AD" {
|
||||
if err := c.UnStageFile(file.Name, false); err != nil {
|
||||
if err := c.UnStageFile(file.Names(), false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
35
pkg/commands/stash_entries_test.go
Normal file
35
pkg/commands/stash_entries_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandStashDo is a function.
|
||||
func TestGitCommandStashDo(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"stash", "drop", "stash@{1}"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.StashDo(1, "drop"))
|
||||
}
|
||||
|
||||
// TestGitCommandStashSave is a function.
|
||||
func TestGitCommandStashSave(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"stash", "save", "A stash message"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.StashSave("A stash message"))
|
||||
}
|
||||
@@ -6,6 +6,13 @@ import (
|
||||
gogit "github.com/jesseduffield/go-git/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
REBASE_MODE_NORMAL = "normal"
|
||||
REBASE_MODE_INTERACTIVE = "interactive"
|
||||
REBASE_MODE_REBASING = "rebasing"
|
||||
REBASE_MODE_MERGING = "merging"
|
||||
)
|
||||
|
||||
// RebaseMode returns "" for non-rebase mode, "normal" for normal rebase
|
||||
// and "interactive" for interactive rebase
|
||||
func (c *GitCommand) RebaseMode() (string, error) {
|
||||
@@ -14,11 +21,11 @@ func (c *GitCommand) RebaseMode() (string, error) {
|
||||
return "", err
|
||||
}
|
||||
if exists {
|
||||
return "normal", nil
|
||||
return REBASE_MODE_NORMAL, nil
|
||||
}
|
||||
exists, err = c.OSCommand.FileExists(filepath.Join(c.DotGitDir, "rebase-merge"))
|
||||
if exists {
|
||||
return "interactive", err
|
||||
return REBASE_MODE_INTERACTIVE, err
|
||||
} else {
|
||||
return "", err
|
||||
}
|
||||
@@ -27,13 +34,13 @@ func (c *GitCommand) RebaseMode() (string, error) {
|
||||
func (c *GitCommand) WorkingTreeState() string {
|
||||
rebaseMode, _ := c.RebaseMode()
|
||||
if rebaseMode != "" {
|
||||
return "rebasing"
|
||||
return REBASE_MODE_REBASING
|
||||
}
|
||||
merging, _ := c.IsInMergeState()
|
||||
if merging {
|
||||
return "merging"
|
||||
return REBASE_MODE_MERGING
|
||||
}
|
||||
return "normal"
|
||||
return REBASE_MODE_NORMAL
|
||||
}
|
||||
|
||||
// IsInMergeState states whether we are still mid-merge
|
||||
|
||||
@@ -69,28 +69,28 @@ func (c *GitCommand) SubmoduleStash(submodule *models.SubmoduleConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.OSCommand.RunCommand("git -C %s stash --include-untracked", submodule.Path)
|
||||
return c.RunCommand("git -C %s stash --include-untracked", submodule.Path)
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleReset(submodule *models.SubmoduleConfig) error {
|
||||
return c.OSCommand.RunCommand("git submodule update --init --force %s", submodule.Path)
|
||||
return c.RunCommand("git submodule update --init --force %s", submodule.Path)
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleUpdateAll() error {
|
||||
// not doing an --init here because the user probably doesn't want that
|
||||
return c.OSCommand.RunCommand("git submodule update --force")
|
||||
return c.RunCommand("git submodule update --force")
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleDelete(submodule *models.SubmoduleConfig) error {
|
||||
// based on https://gist.github.com/myusuf3/7f645819ded92bda6677
|
||||
|
||||
if err := c.OSCommand.RunCommand("git submodule deinit --force %s", submodule.Path); err != nil {
|
||||
if err := c.RunCommand("git submodule deinit --force %s", submodule.Path); err != nil {
|
||||
if strings.Contains(err.Error(), "did not match any file(s) known to git") {
|
||||
if err := c.OSCommand.RunCommand("git config --file .gitmodules --remove-section submodule.%s", submodule.Name); err != nil {
|
||||
if err := c.RunCommand("git config --file .gitmodules --remove-section submodule.%s", submodule.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.OSCommand.RunCommand("git config --remove-section submodule.%s", submodule.Name); err != nil {
|
||||
if err := c.RunCommand("git config --remove-section submodule.%s", submodule.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ func (c *GitCommand) SubmoduleDelete(submodule *models.SubmoduleConfig) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.OSCommand.RunCommand("git rm --force -r %s", submodule.Path); err != nil {
|
||||
if err := c.RunCommand("git rm --force -r %s", submodule.Path); err != nil {
|
||||
// if the directory isn't there then that's fine
|
||||
c.Log.Error(err)
|
||||
}
|
||||
@@ -119,11 +119,11 @@ func (c *GitCommand) SubmoduleAdd(name string, path string, url string) error {
|
||||
|
||||
func (c *GitCommand) SubmoduleUpdateUrl(name string, path string, newUrl string) error {
|
||||
// the set-url command is only for later git versions so we're doing it manually here
|
||||
if err := c.OSCommand.RunCommand("git config --file .gitmodules submodule.%s.url %s", name, newUrl); err != nil {
|
||||
if err := c.RunCommand("git config --file .gitmodules submodule.%s.url %s", name, newUrl); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.OSCommand.RunCommand("git submodule sync %s", path); err != nil {
|
||||
if err := c.RunCommand("git submodule sync %s", path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -131,11 +131,11 @@ func (c *GitCommand) SubmoduleUpdateUrl(name string, path string, newUrl string)
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleInit(path string) error {
|
||||
return c.OSCommand.RunCommand("git submodule init %s", path)
|
||||
return c.RunCommand("git submodule init %s", path)
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleUpdate(path string) error {
|
||||
return c.OSCommand.RunCommand("git submodule update --init %s", path)
|
||||
return c.RunCommand("git submodule update --init %s", path)
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleBulkInitCmdStr() string {
|
||||
|
||||
@@ -2,28 +2,15 @@ package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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().Git.OverrideGpg
|
||||
if overrideGpg {
|
||||
return false
|
||||
}
|
||||
|
||||
gpgsign, _ := c.getLocalGitConfig("commit.gpgsign")
|
||||
if gpgsign == "" {
|
||||
gpgsign, _ = c.getGlobalGitConfig("commit.gpgsign")
|
||||
}
|
||||
value := strings.ToLower(gpgsign)
|
||||
|
||||
return value == "true" || value == "1" || value == "yes" || value == "on"
|
||||
}
|
||||
|
||||
// Push pushes to a branch
|
||||
func (c *GitCommand) Push(branchName string, force bool, upstream string, args string, promptUserForCredential func(string) string) error {
|
||||
followTagsFlag := "--follow-tags"
|
||||
if c.GetConfigValue("push.followTags") == "false" {
|
||||
followTagsFlag = ""
|
||||
}
|
||||
|
||||
forceFlag := ""
|
||||
if force {
|
||||
forceFlag = "--force-with-lease"
|
||||
@@ -34,7 +21,7 @@ func (c *GitCommand) Push(branchName string, force bool, upstream string, args s
|
||||
setUpstreamArg = "--set-upstream " + upstream
|
||||
}
|
||||
|
||||
cmd := fmt.Sprintf("git push --follow-tags %s %s %s", forceFlag, setUpstreamArg, args)
|
||||
cmd := fmt.Sprintf("git push %s %s %s %s", followTagsFlag, forceFlag, setUpstreamArg, args)
|
||||
return c.OSCommand.DetectUnamePass(cmd, promptUserForCredential)
|
||||
}
|
||||
|
||||
|
||||
98
pkg/commands/sync_test.go
Normal file
98
pkg/commands/sync_test.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandPush is a function.
|
||||
func TestGitCommandPush(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
getGitConfigValue func(string) (string, error)
|
||||
command func(string, ...string) *exec.Cmd
|
||||
forcePush bool
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Push with force disabled, follow-tags on",
|
||||
func(string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push", "--follow-tags"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
false,
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with force enabled, follow-tags on",
|
||||
func(string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push", "--follow-tags", "--force-with-lease"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
true,
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with force disabled, follow-tags off",
|
||||
func(string) (string, error) {
|
||||
return "false", nil
|
||||
},
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
false,
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with an error occurring, follow-tags on",
|
||||
func(string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push", "--follow-tags"}, args)
|
||||
return secureexec.Command("test")
|
||||
},
|
||||
false,
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
gitCmd.getGitConfigValue = s.getGitConfigValue
|
||||
err := gitCmd.Push("test", s.forcePush, "", "", func(passOrUname string) string {
|
||||
return "\n"
|
||||
})
|
||||
s.test(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,11 @@ package commands
|
||||
import "fmt"
|
||||
|
||||
func (c *GitCommand) CreateLightweightTag(tagName string, commitSha string) error {
|
||||
return c.OSCommand.RunCommand("git tag %s %s", tagName, commitSha)
|
||||
return c.RunCommand("git tag %s %s", tagName, commitSha)
|
||||
}
|
||||
|
||||
func (c *GitCommand) DeleteTag(tagName string) error {
|
||||
return c.OSCommand.RunCommand("git tag -d %s", tagName)
|
||||
return c.RunCommand("git tag -d %s", tagName)
|
||||
}
|
||||
|
||||
func (c *GitCommand) PushTag(remoteName string, tagName string, promptUserForCredential func(string) string) error {
|
||||
|
||||
@@ -41,6 +41,7 @@ type AppConfigurer interface {
|
||||
SaveAppState() error
|
||||
SetIsNewRepo(bool)
|
||||
GetIsNewRepo() bool
|
||||
ReloadUserConfig() error
|
||||
}
|
||||
|
||||
// NewAppConfig makes a new app config
|
||||
@@ -82,20 +83,25 @@ func NewAppConfig(name, version, commit, date string, buildSource string, debugg
|
||||
}
|
||||
|
||||
func ConfigDir() string {
|
||||
legacyConfigDirectory := configDirForVendor("jesseduffield")
|
||||
if _, err := os.Stat(legacyConfigDirectory); !os.IsNotExist(err) {
|
||||
return legacyConfigDirectory
|
||||
}
|
||||
configDirectory := configDirForVendor("")
|
||||
return configDirectory
|
||||
}
|
||||
|
||||
func configDirForVendor(vendor string) string {
|
||||
envConfigDir := os.Getenv("CONFIG_DIR")
|
||||
if envConfigDir != "" {
|
||||
return envConfigDir
|
||||
}
|
||||
|
||||
// chucking my name there is not for vanity purposes, the xdg spec (and that
|
||||
// function) requires a vendor name. May as well line up with github
|
||||
configDirs := xdg.New("jesseduffield", "lazygit")
|
||||
configDirs := xdg.New(vendor, "lazygit")
|
||||
return configDirs.ConfigHome()
|
||||
}
|
||||
|
||||
func findOrCreateConfigDir() (string, error) {
|
||||
folder := ConfigDir()
|
||||
|
||||
err := os.MkdirAll(folder, 0755)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -198,6 +204,16 @@ func (c *AppConfig) GetUserConfigDir() string {
|
||||
return c.UserConfigDir
|
||||
}
|
||||
|
||||
func (c *AppConfig) ReloadUserConfig() error {
|
||||
userConfig, err := loadUserConfigWithDefaults(c.UserConfigDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.UserConfig = userConfig
|
||||
return nil
|
||||
}
|
||||
|
||||
func configFilePath(filename string) (string, error) {
|
||||
folder, err := findOrCreateConfigDir()
|
||||
if err != nil {
|
||||
|
||||
@@ -4,6 +4,7 @@ type UserConfig struct {
|
||||
Gui GuiConfig `yaml:"gui"`
|
||||
Git GitConfig `yaml:"git"`
|
||||
Update UpdateConfig `yaml:"update"`
|
||||
Refresher RefresherConfig `yaml:"refresher"`
|
||||
Reporting string `yaml:"reporting"`
|
||||
SplashUpdatesIndex int `yaml:"splashUpdatesIndex"`
|
||||
ConfirmOnQuit bool `yaml:"confirmOnQuit"`
|
||||
@@ -14,19 +15,30 @@ type UserConfig struct {
|
||||
DisableStartupPopups bool `yaml:"disableStartupPopups"`
|
||||
CustomCommands []CustomCommand `yaml:"customCommands"`
|
||||
Services map[string]string `yaml:"services"`
|
||||
NotARepository string `yaml:"notARepository"`
|
||||
}
|
||||
|
||||
type RefresherConfig struct {
|
||||
RefreshInterval int `yaml:"refreshInterval"`
|
||||
FetchInterval int `yaml:"fetchInterval"`
|
||||
}
|
||||
|
||||
type GuiConfig struct {
|
||||
ScrollHeight int `yaml:"scrollHeight"`
|
||||
ScrollPastBottom bool `yaml:"scrollPastBottom"`
|
||||
MouseEvents bool `yaml:"mouseEvents"`
|
||||
SkipUnstageLineWarning bool `yaml:"skipUnstageLineWarning"`
|
||||
SkipStashWarning bool `yaml:"skipStashWarning"`
|
||||
SidePanelWidth float64 `yaml:"sidePanelWidth"`
|
||||
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
|
||||
MainPanelSplitMode string `yaml:"mainPanelSplitMode"`
|
||||
Theme ThemeConfig `yaml:"theme"`
|
||||
CommitLength CommitLengthConfig `yaml:"commitLength"`
|
||||
ScrollHeight int `yaml:"scrollHeight"`
|
||||
ScrollPastBottom bool `yaml:"scrollPastBottom"`
|
||||
MouseEvents bool `yaml:"mouseEvents"`
|
||||
SkipUnstageLineWarning bool `yaml:"skipUnstageLineWarning"`
|
||||
SkipStashWarning bool `yaml:"skipStashWarning"`
|
||||
SidePanelWidth float64 `yaml:"sidePanelWidth"`
|
||||
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
|
||||
MainPanelSplitMode string `yaml:"mainPanelSplitMode"`
|
||||
Theme ThemeConfig `yaml:"theme"`
|
||||
CommitLength CommitLengthConfig `yaml:"commitLength"`
|
||||
SkipNoStagedFilesWarning bool `yaml:"skipNoStagedFilesWarning"`
|
||||
ShowFileTree bool `yaml:"showFileTree"`
|
||||
ShowRandomTip bool `yaml:"showRandomTip"`
|
||||
ShowCommandLog bool `yaml:"showCommandLog"`
|
||||
CommandLogSize int `yaml:"commandLogSize"`
|
||||
}
|
||||
|
||||
type ThemeConfig struct {
|
||||
@@ -49,6 +61,7 @@ type GitConfig struct {
|
||||
SkipHookPrefix string `yaml:"skipHookPrefix"`
|
||||
AutoFetch bool `yaml:"autoFetch"`
|
||||
BranchLogCmd string `yaml:"branchLogCmd"`
|
||||
AllBranchesLogCmd string `yaml:"allBranchesLogCmd"`
|
||||
OverrideGpg bool `yaml:"overrideGpg"`
|
||||
DisableForcePushing bool `yaml:"disableForcePushing"`
|
||||
CommitPrefixes map[string]CommitPrefixConfig `yaml:"commitPrefixes"`
|
||||
@@ -91,6 +104,7 @@ type KeybindingConfig struct {
|
||||
Submodules KeybindingSubmodulesConfig `yaml:"submodules"`
|
||||
}
|
||||
|
||||
// damn looks like we have some inconsistencies here with -alt and -alt1
|
||||
type KeybindingUniversalConfig struct {
|
||||
Quit string `yaml:"quit"`
|
||||
QuitAlt1 string `yaml:"quit-alt1"`
|
||||
@@ -109,6 +123,8 @@ type KeybindingUniversalConfig struct {
|
||||
NextBlock string `yaml:"nextBlock"`
|
||||
PrevBlockAlt string `yaml:"prevBlock-alt"`
|
||||
NextBlockAlt string `yaml:"nextBlock-alt"`
|
||||
NextBlockAlt2 string `yaml:"nextBlock-alt2"`
|
||||
PrevBlockAlt2 string `yaml:"prevBlock-alt2"`
|
||||
NextMatch string `yaml:"nextMatch"`
|
||||
PrevMatch string `yaml:"prevMatch"`
|
||||
StartSearch string `yaml:"startSearch"`
|
||||
@@ -146,11 +162,13 @@ type KeybindingUniversalConfig struct {
|
||||
CopyToClipboard string `yaml:"copyToClipboard"`
|
||||
SubmitEditorText string `yaml:"submitEditorText"`
|
||||
AppendNewline string `yaml:"appendNewline"`
|
||||
ExtrasMenu string `yaml:"extrasMenu"`
|
||||
}
|
||||
|
||||
type KeybindingStatusConfig struct {
|
||||
CheckForUpdate string `yaml:"checkForUpdate"`
|
||||
RecentRepos string `yaml:"recentRepos"`
|
||||
CheckForUpdate string `yaml:"checkForUpdate"`
|
||||
RecentRepos string `yaml:"recentRepos"`
|
||||
AllBranchesLogGraph string `yaml:"allBranchesLogGraph"`
|
||||
}
|
||||
|
||||
type KeybindingFilesConfig struct {
|
||||
@@ -165,10 +183,13 @@ type KeybindingFilesConfig struct {
|
||||
ToggleStagedAll string `yaml:"toggleStagedAll"`
|
||||
ViewResetOptions string `yaml:"viewResetOptions"`
|
||||
Fetch string `yaml:"fetch"`
|
||||
ToggleTreeView string `yaml:"toggleTreeView"`
|
||||
OpenMergeTool string `yaml:"openMergeTool"`
|
||||
}
|
||||
|
||||
type KeybindingBranchesConfig struct {
|
||||
CreatePullRequest string `yaml:"createPullRequest"`
|
||||
CopyPullRequestURL string `yaml:"copyPullRequestURL"`
|
||||
CheckoutBranchByName string `yaml:"checkoutBranchByName"`
|
||||
ForceCheckoutBranch string `yaml:"forceCheckoutBranch"`
|
||||
RebaseBranch string `yaml:"rebaseBranch"`
|
||||
@@ -279,7 +300,12 @@ func GetDefaultConfig() *UserConfig {
|
||||
SelectedLineBgColor: []string{"default"},
|
||||
SelectedRangeBgColor: []string{"blue"},
|
||||
},
|
||||
CommitLength: CommitLengthConfig{Show: true},
|
||||
CommitLength: CommitLengthConfig{Show: true},
|
||||
SkipNoStagedFilesWarning: false,
|
||||
ShowCommandLog: true,
|
||||
ShowFileTree: false,
|
||||
ShowRandomTip: true,
|
||||
CommandLogSize: 8,
|
||||
},
|
||||
Git: GitConfig{
|
||||
Paging: PagingConfig{
|
||||
@@ -296,10 +322,14 @@ func GetDefaultConfig() *UserConfig {
|
||||
SkipHookPrefix: "WIP",
|
||||
AutoFetch: true,
|
||||
BranchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --",
|
||||
OverrideGpg: false,
|
||||
AllBranchesLogCmd: "git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium",
|
||||
DisableForcePushing: false,
|
||||
CommitPrefixes: map[string]CommitPrefixConfig(nil),
|
||||
},
|
||||
Refresher: RefresherConfig{
|
||||
RefreshInterval: 10,
|
||||
FetchInterval: 60,
|
||||
},
|
||||
Update: UpdateConfig{
|
||||
Method: "prompt",
|
||||
Days: 14,
|
||||
@@ -307,7 +337,7 @@ func GetDefaultConfig() *UserConfig {
|
||||
Reporting: "undetermined",
|
||||
SplashUpdatesIndex: 0,
|
||||
ConfirmOnQuit: false,
|
||||
QuitOnTopLevelReturn: true,
|
||||
QuitOnTopLevelReturn: false,
|
||||
Keybinding: KeybindingConfig{
|
||||
Universal: KeybindingUniversalConfig{
|
||||
Quit: "q",
|
||||
@@ -327,6 +357,8 @@ func GetDefaultConfig() *UserConfig {
|
||||
NextBlock: "<right>",
|
||||
PrevBlockAlt: "h",
|
||||
NextBlockAlt: "l",
|
||||
PrevBlockAlt2: "<backtab>",
|
||||
NextBlockAlt2: "<tab>",
|
||||
NextMatch: "n",
|
||||
PrevMatch: "N",
|
||||
StartSearch: "/",
|
||||
@@ -363,11 +395,13 @@ func GetDefaultConfig() *UserConfig {
|
||||
DiffingMenuAlt: "<c-e>",
|
||||
CopyToClipboard: "<c-o>",
|
||||
SubmitEditorText: "<enter>",
|
||||
AppendNewline: "<tab>",
|
||||
AppendNewline: "<a-enter>",
|
||||
ExtrasMenu: "@",
|
||||
},
|
||||
Status: KeybindingStatusConfig{
|
||||
CheckForUpdate: "u",
|
||||
RecentRepos: "<enter>",
|
||||
CheckForUpdate: "u",
|
||||
RecentRepos: "<enter>",
|
||||
AllBranchesLogGraph: "a",
|
||||
},
|
||||
Files: KeybindingFilesConfig{
|
||||
CommitChanges: "c",
|
||||
@@ -381,8 +415,11 @@ func GetDefaultConfig() *UserConfig {
|
||||
ToggleStagedAll: "a",
|
||||
ViewResetOptions: "D",
|
||||
Fetch: "f",
|
||||
ToggleTreeView: "`",
|
||||
OpenMergeTool: "M",
|
||||
},
|
||||
Branches: KeybindingBranchesConfig{
|
||||
CopyPullRequestURL: "<c-y>",
|
||||
CreatePullRequest: "o",
|
||||
CheckoutBranchByName: "c",
|
||||
ForceCheckoutBranch: "F",
|
||||
@@ -438,5 +475,6 @@ func GetDefaultConfig() *UserConfig {
|
||||
DisableStartupPopups: false,
|
||||
CustomCommands: []CustomCommand(nil),
|
||||
Services: map[string]string(nil),
|
||||
NotARepository: "prompt",
|
||||
}
|
||||
}
|
||||
|
||||
35
pkg/constants/links.go
Normal file
35
pkg/constants/links.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package constants
|
||||
|
||||
type Docs struct {
|
||||
CustomPagers string
|
||||
CustomCommands string
|
||||
CustomKeybindings string
|
||||
Keybindings string
|
||||
Undoing string
|
||||
Config string
|
||||
Tutorial string
|
||||
}
|
||||
|
||||
var Links = struct {
|
||||
Docs Docs
|
||||
Issues string
|
||||
Donate string
|
||||
Discussions string
|
||||
RepoUrl string
|
||||
Releases string
|
||||
}{
|
||||
RepoUrl: "https://github.com/jesseduffield/lazygit",
|
||||
Issues: "https://github.com/jesseduffield/lazygit/issues",
|
||||
Donate: "https://github.com/sponsors/jesseduffield",
|
||||
Discussions: "https://github.com/jesseduffield/lazygit/discussions",
|
||||
Releases: "https://github.com/jesseduffield/lazygit/releases",
|
||||
Docs: Docs{
|
||||
CustomPagers: "https://github.com/jesseduffield/lazygit/blob/master/docs/Custom_Pagers.md",
|
||||
CustomKeybindings: "https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md",
|
||||
CustomCommands: "https://github.com/jesseduffield/lazygit/wiki/Custom-Commands-Compendium",
|
||||
Keybindings: "https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings",
|
||||
Undoing: "https://github.com/jesseduffield/lazygit/blob/master/docs/Undoing.md",
|
||||
Config: "https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md",
|
||||
Tutorial: "https://youtu.be/VDXvbHZYeKY",
|
||||
},
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
@@ -8,33 +9,68 @@ import (
|
||||
)
|
||||
|
||||
type appStatus struct {
|
||||
name string
|
||||
message string
|
||||
statusType string
|
||||
duration int
|
||||
id int
|
||||
}
|
||||
|
||||
type statusManager struct {
|
||||
statuses []appStatus
|
||||
nextId int
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
func (m *statusManager) removeStatus(name string) {
|
||||
func (m *statusManager) removeStatus(id int) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
newStatuses := []appStatus{}
|
||||
for _, status := range m.statuses {
|
||||
if status.name != name {
|
||||
if status.id != id {
|
||||
newStatuses = append(newStatuses, status)
|
||||
}
|
||||
}
|
||||
m.statuses = newStatuses
|
||||
}
|
||||
|
||||
func (m *statusManager) addWaitingStatus(name string) {
|
||||
m.removeStatus(name)
|
||||
func (m *statusManager) addWaitingStatus(message string) int {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
m.nextId += 1
|
||||
id := m.nextId
|
||||
|
||||
newStatus := appStatus{
|
||||
name: name,
|
||||
message: message,
|
||||
statusType: "waiting",
|
||||
duration: 0,
|
||||
id: id,
|
||||
}
|
||||
m.statuses = append([]appStatus{newStatus}, m.statuses...)
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func (m *statusManager) addToastStatus(message string) int {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
m.nextId++
|
||||
id := m.nextId
|
||||
|
||||
newStatus := appStatus{
|
||||
message: message,
|
||||
statusType: "toast",
|
||||
id: id,
|
||||
}
|
||||
m.statuses = append([]appStatus{newStatus}, m.statuses...)
|
||||
|
||||
go func() {
|
||||
time.Sleep(time.Second * 2)
|
||||
|
||||
m.removeStatus(id)
|
||||
}()
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func (m *statusManager) getStatusString() string {
|
||||
@@ -43,31 +79,42 @@ func (m *statusManager) getStatusString() string {
|
||||
}
|
||||
topStatus := m.statuses[0]
|
||||
if topStatus.statusType == "waiting" {
|
||||
return topStatus.name + " " + utils.Loader()
|
||||
return topStatus.message + " " + utils.Loader()
|
||||
}
|
||||
return topStatus.name
|
||||
return topStatus.message
|
||||
}
|
||||
|
||||
func (gui *Gui) raiseToast(message string) {
|
||||
gui.statusManager.addToastStatus(message)
|
||||
|
||||
gui.renderAppStatus()
|
||||
}
|
||||
|
||||
func (gui *Gui) renderAppStatus() {
|
||||
go utils.Safe(func() {
|
||||
ticker := time.NewTicker(time.Millisecond * 50)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
appStatus := gui.statusManager.getStatusString()
|
||||
if appStatus == "" {
|
||||
gui.renderString(gui.Views.AppStatus, "")
|
||||
return
|
||||
}
|
||||
gui.renderString(gui.Views.AppStatus, appStatus)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// WithWaitingStatus wraps a function and shows a waiting status while the function is still executing
|
||||
func (gui *Gui) WithWaitingStatus(name string, f func() error) error {
|
||||
func (gui *Gui) WithWaitingStatus(message string, f func() error) error {
|
||||
go utils.Safe(func() {
|
||||
gui.statusManager.addWaitingStatus(name)
|
||||
id := gui.statusManager.addWaitingStatus(message)
|
||||
|
||||
defer func() {
|
||||
gui.statusManager.removeStatus(name)
|
||||
gui.statusManager.removeStatus(id)
|
||||
}()
|
||||
|
||||
go utils.Safe(func() {
|
||||
ticker := time.NewTicker(time.Millisecond * 50)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
appStatus := gui.statusManager.getStatusString()
|
||||
if appStatus == "" {
|
||||
return
|
||||
}
|
||||
gui.renderString("appStatus", appStatus)
|
||||
}
|
||||
})
|
||||
gui.renderAppStatus()
|
||||
|
||||
if err := f(); err != nil {
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
const INFO_SECTION_PADDING = " "
|
||||
|
||||
func (gui *Gui) mainSectionChildren() []*boxlayout.Box {
|
||||
currentWindow := gui.currentWindow()
|
||||
|
||||
@@ -130,6 +132,24 @@ func (gui *Gui) splitMainPanelSideBySide() bool {
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) getExtrasWindowSize(screenHeight int) int {
|
||||
if !gui.ShowExtrasWindow {
|
||||
return 0
|
||||
}
|
||||
|
||||
var baseSize int
|
||||
if gui.currentStaticContext().GetKey() == COMMAND_LOG_CONTEXT_KEY {
|
||||
baseSize = 1000 // my way of saying 'fill the available space'
|
||||
} else if screenHeight < 40 {
|
||||
baseSize = 1
|
||||
} else {
|
||||
baseSize = gui.Config.GetUserConfig().Gui.CommandLogSize
|
||||
}
|
||||
|
||||
frameSize := 2
|
||||
return baseSize + frameSize
|
||||
}
|
||||
|
||||
func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions {
|
||||
width, height := gui.g.Size()
|
||||
|
||||
@@ -146,6 +166,8 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
|
||||
mainPanelsDirection = boxlayout.COLUMN
|
||||
}
|
||||
|
||||
extrasWindowSize := gui.getExtrasWindowSize(height)
|
||||
|
||||
root := &boxlayout.Box{
|
||||
Direction: boxlayout.ROW,
|
||||
Children: []*boxlayout.Box{
|
||||
@@ -159,9 +181,19 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
|
||||
ConditionalChildren: gui.sidePanelChildren,
|
||||
},
|
||||
{
|
||||
Direction: mainPanelsDirection,
|
||||
Direction: boxlayout.ROW,
|
||||
Weight: mainSectionWeight,
|
||||
Children: gui.mainSectionChildren(),
|
||||
Children: []*boxlayout.Box{
|
||||
{
|
||||
Direction: mainPanelsDirection,
|
||||
Children: gui.mainSectionChildren(),
|
||||
Weight: 1,
|
||||
},
|
||||
{
|
||||
Window: "extras",
|
||||
Size: extrasWindowSize,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -181,9 +213,12 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
|
||||
// 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 {
|
||||
gui.State.ContextManager.RLock()
|
||||
defer gui.State.ContextManager.RUnlock()
|
||||
|
||||
box := &boxlayout.Box{Window: "stash"}
|
||||
stashWindowAccessed := false
|
||||
for _, context := range gui.State.ContextStack {
|
||||
for _, context := range gui.State.ContextManager.ContextStack {
|
||||
if context.GetWindowName() == "stash" {
|
||||
stashWindowAccessed = true
|
||||
}
|
||||
@@ -278,9 +313,12 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
|
||||
|
||||
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]
|
||||
gui.State.ContextManager.RLock()
|
||||
defer gui.State.ContextManager.RUnlock()
|
||||
|
||||
for idx := range gui.State.ContextManager.ContextStack {
|
||||
reversedIdx := len(gui.State.ContextManager.ContextStack) - 1 - idx
|
||||
context := gui.State.ContextManager.ContextStack[reversedIdx]
|
||||
|
||||
if context.GetKind() == SIDE_CONTEXT {
|
||||
return context.GetWindowName()
|
||||
|
||||
77
pkg/gui/basic_context.go
Normal file
77
pkg/gui/basic_context.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package gui
|
||||
|
||||
type BasicContext struct {
|
||||
OnFocus func() error
|
||||
OnFocusLost func() error
|
||||
OnRender func() error
|
||||
Kind ContextKind
|
||||
Key ContextKey
|
||||
ViewName string
|
||||
WindowName string
|
||||
OnGetOptionsMap func() map[string]string
|
||||
|
||||
ParentContext Context
|
||||
// we can't know on the calling end whether a Context is actually a nil value without reflection, so we're storing this flag here to tell us. There has got to be a better way around this
|
||||
hasParent bool
|
||||
}
|
||||
|
||||
func (c *BasicContext) GetOptionsMap() map[string]string {
|
||||
if c.OnGetOptionsMap != nil {
|
||||
return c.OnGetOptionsMap()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *BasicContext) SetParentContext(context Context) {
|
||||
c.ParentContext = context
|
||||
c.hasParent = true
|
||||
}
|
||||
|
||||
func (c *BasicContext) GetParentContext() (Context, bool) {
|
||||
return c.ParentContext, c.hasParent
|
||||
}
|
||||
|
||||
func (c *BasicContext) SetWindowName(windowName string) {
|
||||
c.WindowName = windowName
|
||||
}
|
||||
|
||||
func (c *BasicContext) GetWindowName() string {
|
||||
windowName := c.WindowName
|
||||
|
||||
if windowName != "" {
|
||||
return windowName
|
||||
}
|
||||
|
||||
// TODO: actually set this for everything so we don't default to the view name
|
||||
return c.ViewName
|
||||
}
|
||||
|
||||
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() ContextKind {
|
||||
return c.Kind
|
||||
}
|
||||
|
||||
func (c *BasicContext) GetKey() ContextKey {
|
||||
return c.Key
|
||||
}
|
||||
@@ -9,8 +9,10 @@ type Dimensions struct {
|
||||
Y1 int
|
||||
}
|
||||
|
||||
type Direction int
|
||||
|
||||
const (
|
||||
ROW = iota
|
||||
ROW Direction = iota
|
||||
COLUMN
|
||||
)
|
||||
|
||||
@@ -26,10 +28,10 @@ const (
|
||||
|
||||
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
|
||||
Direction Direction
|
||||
|
||||
// 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
|
||||
ConditionalDirection func(width int, height int) Direction
|
||||
|
||||
Children []*Box
|
||||
|
||||
@@ -94,6 +96,11 @@ func ArrangeWindows(root *Box, x0, y0, width, height int) map[string]Dimensions
|
||||
var boxSize int
|
||||
if child.isStatic() {
|
||||
boxSize = child.Size
|
||||
// assuming that only one static child can have a size greater than the
|
||||
// available space. In that case we just crop the size to what's available
|
||||
if boxSize > availableSize {
|
||||
boxSize = availableSize
|
||||
}
|
||||
} else {
|
||||
// TODO: consider more evenly distributing the remainder
|
||||
boxSize = unitSize * child.Weight
|
||||
@@ -120,7 +127,7 @@ func (b *Box) isStatic() bool {
|
||||
return b.Size > 0
|
||||
}
|
||||
|
||||
func (b *Box) getDirection(width int, height int) int {
|
||||
func (b *Box) getDirection(width int, height int) Direction {
|
||||
if b.ConditionalDirection != nil {
|
||||
return b.ConditionalDirection(width, height)
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ func TestArrangeWindows(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"Box with COLUMN direction only on wide boxes with narrow box",
|
||||
&Box{ConditionalDirection: func(width int, height int) int {
|
||||
&Box{ConditionalDirection: func(width int, height int) Direction {
|
||||
if width > 4 {
|
||||
return COLUMN
|
||||
} else {
|
||||
@@ -111,7 +111,7 @@ func TestArrangeWindows(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"Box with COLUMN direction only on wide boxes with wide box",
|
||||
&Box{ConditionalDirection: func(width int, height int) int {
|
||||
&Box{ConditionalDirection: func(width int, height int) Direction {
|
||||
if width > 4 {
|
||||
return COLUMN
|
||||
} else {
|
||||
@@ -179,9 +179,31 @@ func TestArrangeWindows(t *testing.T) {
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Box with static child with size too large",
|
||||
&Box{Direction: COLUMN, Children: []*Box{{Size: 11, Window: "static"}, {Weight: 1, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}}},
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
10,
|
||||
func(result map[string]Dimensions) {
|
||||
assert.EqualValues(
|
||||
t,
|
||||
result,
|
||||
map[string]Dimensions{
|
||||
"static": {X0: 0, X1: 9, Y0: 0, Y1: 9},
|
||||
// not sure if X0: 10, X1: 9 makes any sense, but testing this in the
|
||||
// actual GUI it seems harmless
|
||||
"dynamic1": {X0: 10, X1: 9, Y0: 0, Y1: 9},
|
||||
"dynamic2": {X0: 10, X1: 9, Y0: 0, Y1: 9},
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
s.test(ArrangeWindows(s.root, s.x0, s.y0, s.width, s.height))
|
||||
})
|
||||
|
||||
@@ -4,9 +4,11 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -29,13 +31,13 @@ func (gui *Gui) handleBranchSelect() error {
|
||||
var task updateTask
|
||||
branch := gui.getSelectedBranch()
|
||||
if branch == nil {
|
||||
task = gui.createRenderStringTask(gui.Tr.NoBranchesThisRepo)
|
||||
task = NewRenderStringTask(gui.Tr.NoBranchesThisRepo)
|
||||
} else {
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.GetBranchGraphCmdStr(branch.Name),
|
||||
)
|
||||
|
||||
task = gui.createRunPtyTask(cmd)
|
||||
task = NewRunPtyTask(cmd)
|
||||
}
|
||||
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
@@ -68,7 +70,7 @@ func (gui *Gui) refreshBranches() {
|
||||
}
|
||||
gui.State.Branches = builder.Build()
|
||||
|
||||
if err := gui.postRefreshUpdate(gui.Contexts.Branches.Context); err != nil {
|
||||
if err := gui.postRefreshUpdate(gui.State.Contexts.Branches); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
|
||||
@@ -77,7 +79,7 @@ func (gui *Gui) refreshBranches() {
|
||||
|
||||
// specific functions
|
||||
|
||||
func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleBranchPress() error {
|
||||
if gui.State.Panels.Branches.SelectedLineIdx == -1 {
|
||||
return nil
|
||||
}
|
||||
@@ -85,33 +87,50 @@ func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.createErrorPanel(gui.Tr.AlreadyCheckedOutBranch)
|
||||
}
|
||||
branch := gui.getSelectedBranch()
|
||||
return gui.handleCheckoutRef(branch.Name, handleCheckoutRefOptions{})
|
||||
return gui.handleCheckoutRef(branch.Name, handleCheckoutRefOptions{span: gui.Tr.Spans.CheckoutBranch})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreatePullRequestPress(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCreatePullRequestPress() error {
|
||||
pullRequest := commands.NewPullRequest(gui.GitCommand)
|
||||
|
||||
branch := gui.getSelectedBranch()
|
||||
if err := pullRequest.Create(branch); err != nil {
|
||||
url, err := pullRequest.Create(branch)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(fmt.Sprintf("Creating pull request at URL: %s", url), "Create pull request", false))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleGitFetch(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.createLoaderPanel(v, gui.Tr.FetchWait); err != nil {
|
||||
func (gui *Gui) handleCopyPullRequestURLPress() error {
|
||||
pullRequest := commands.NewPullRequest(gui.GitCommand)
|
||||
|
||||
branch := gui.getSelectedBranch()
|
||||
url, err := pullRequest.CopyURL(branch)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(fmt.Sprintf("Copying to clipboard: '%s'", url), "Copy URL", false))
|
||||
|
||||
gui.raiseToast(gui.Tr.PullRequestURLCopiedToClipboard)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleGitFetch() error {
|
||||
if err := gui.createLoaderPanel(gui.Tr.FetchWait); err != nil {
|
||||
return err
|
||||
}
|
||||
go utils.Safe(func() {
|
||||
err := gui.fetch(true)
|
||||
err := gui.fetch(true, "Fetch")
|
||||
gui.handleCredentialsPopup(err)
|
||||
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleForceCheckout() error {
|
||||
branch := gui.getSelectedBranch()
|
||||
message := gui.Tr.SureForceCheckout
|
||||
title := gui.Tr.ForceCheckoutBranch
|
||||
@@ -120,7 +139,7 @@ func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
|
||||
title: title,
|
||||
prompt: message,
|
||||
handleConfirm: func() error {
|
||||
if err := gui.GitCommand.Checkout(branch.Name, commands.CheckoutOptions{Force: true}); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.ForceCheckoutBranch).Checkout(branch.Name, commands.CheckoutOptions{Force: true}); err != nil {
|
||||
_ = gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
@@ -132,6 +151,7 @@ type handleCheckoutRefOptions struct {
|
||||
WaitingStatus string
|
||||
EnvVars []string
|
||||
onRefNotFound func(ref string) error
|
||||
span string
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions) error {
|
||||
@@ -149,8 +169,10 @@ func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions)
|
||||
gui.State.Panels.Commits.LimitCommits = true
|
||||
}
|
||||
|
||||
gitCommand := gui.GitCommand.WithSpan(options.span)
|
||||
|
||||
return gui.WithWaitingStatus(waitingStatus, func() error {
|
||||
if err := gui.GitCommand.Checkout(ref, cmdOptions); err != nil {
|
||||
if err := 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 options.onRefNotFound != nil && strings.Contains(err.Error(), "did not match any file(s) known to git") {
|
||||
@@ -164,15 +186,15 @@ func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions)
|
||||
title: gui.Tr.AutoStashTitle,
|
||||
prompt: gui.Tr.AutoStashPrompt,
|
||||
handleConfirm: func() error {
|
||||
if err := gui.GitCommand.StashSave(gui.Tr.StashPrefix + ref); err != nil {
|
||||
if err := gitCommand.StashSave(gui.Tr.StashPrefix + ref); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
if err := gui.GitCommand.Checkout(ref, cmdOptions); err != nil {
|
||||
if err := gitCommand.Checkout(ref, cmdOptions); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
onSuccess()
|
||||
if err := gui.GitCommand.StashDo(0, "pop"); err != nil {
|
||||
if err := gitCommand.StashDo(0, "pop"); err != nil {
|
||||
if err := gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI}); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -193,22 +215,26 @@ func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions)
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCheckoutByName(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.prompt(gui.Tr.BranchName+":", "", func(response string) error {
|
||||
return gui.handleCheckoutRef(response, handleCheckoutRefOptions{
|
||||
onRefNotFound: func(ref string) error {
|
||||
func (gui *Gui) handleCheckoutByName() error {
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.BranchName + ":",
|
||||
findSuggestionsFunc: gui.findBranchNameSuggestions,
|
||||
handleConfirm: func(response string) error {
|
||||
return gui.handleCheckoutRef(response, handleCheckoutRefOptions{
|
||||
span: "Checkout branch",
|
||||
onRefNotFound: func(ref string) error {
|
||||
|
||||
return gui.ask(askOpts{
|
||||
|
||||
title: gui.Tr.BranchNotFoundTitle,
|
||||
prompt: fmt.Sprintf("%s %s%s", gui.Tr.BranchNotFoundPrompt, ref, "?"),
|
||||
handleConfirm: func() error {
|
||||
return gui.createNewBranchWithName(ref)
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
})
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.BranchNotFoundTitle,
|
||||
prompt: fmt.Sprintf("%s %s%s", gui.Tr.BranchNotFoundPrompt, ref, "?"),
|
||||
handleConfirm: func() error {
|
||||
return gui.createNewBranchWithName(ref)
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
}},
|
||||
)
|
||||
}
|
||||
|
||||
func (gui *Gui) getCheckedOutBranch() *models.Branch {
|
||||
@@ -233,7 +259,7 @@ func (gui *Gui) createNewBranchWithName(newBranchName string) error {
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleDeleteBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleDeleteBranch() error {
|
||||
return gui.deleteBranch(false)
|
||||
}
|
||||
|
||||
@@ -265,18 +291,17 @@ func (gui *Gui) deleteNamedBranch(selectedBranch *models.Branch, force bool) err
|
||||
)
|
||||
|
||||
return gui.ask(askOpts{
|
||||
|
||||
title: title,
|
||||
prompt: message,
|
||||
handleConfirm: func() error {
|
||||
if err := gui.GitCommand.DeleteBranch(selectedBranch.Name, force); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.DeleteBranch).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.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{BRANCHES}})
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{BRANCHES}})
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -302,17 +327,16 @@ func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error {
|
||||
)
|
||||
|
||||
return gui.ask(askOpts{
|
||||
|
||||
title: gui.Tr.MergingTitle,
|
||||
prompt: prompt,
|
||||
handleConfirm: func() error {
|
||||
err := gui.GitCommand.Merge(branchName, commands.MergeOpts{})
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.Merge).Merge(branchName, commands.MergeOpts{})
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleMerge() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
@@ -321,7 +345,7 @@ func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.mergeBranchIntoCheckedOutBranch(selectedBranchName)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRebaseOntoLocalBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleRebaseOntoLocalBranch() error {
|
||||
selectedBranchName := gui.getSelectedBranch().Name
|
||||
return gui.handleRebaseOntoBranch(selectedBranchName)
|
||||
}
|
||||
@@ -344,17 +368,16 @@ func (gui *Gui) handleRebaseOntoBranch(selectedBranchName string) error {
|
||||
)
|
||||
|
||||
return gui.ask(askOpts{
|
||||
|
||||
title: gui.Tr.RebasingTitle,
|
||||
prompt: prompt,
|
||||
handleConfirm: func() error {
|
||||
err := gui.GitCommand.RebaseBranch(selectedBranchName)
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.RebaseBranch).RebaseBranch(selectedBranchName)
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleFastForward() error {
|
||||
branch := gui.getSelectedBranch()
|
||||
if branch == nil {
|
||||
return nil
|
||||
@@ -374,6 +397,8 @@ func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
span := gui.Tr.Spans.FastForwardBranch
|
||||
|
||||
split := strings.Split(upstream, "/")
|
||||
remoteName := split[0]
|
||||
remoteBranchName := strings.Join(split[1:], "/")
|
||||
@@ -386,20 +411,20 @@ func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
|
||||
},
|
||||
)
|
||||
go utils.Safe(func() {
|
||||
_ = gui.createLoaderPanel(v, message)
|
||||
_ = gui.createLoaderPanel(message)
|
||||
|
||||
if gui.State.Panels.Branches.SelectedLineIdx == 0 {
|
||||
_ = gui.pullWithMode("ff-only", PullFilesOptions{})
|
||||
_ = gui.pullWithMode("ff-only", PullFilesOptions{span: span})
|
||||
} else {
|
||||
err := gui.GitCommand.FastForward(branch.Name, remoteName, remoteBranchName, gui.promptUserForCredential)
|
||||
err := gui.GitCommand.WithSpan(span).FastForward(branch.Name, remoteName, remoteBranchName, gui.promptUserForCredential)
|
||||
gui.handleCredentialsPopup(err)
|
||||
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{BRANCHES}})
|
||||
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{BRANCHES}})
|
||||
}
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateResetToBranchMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCreateResetToBranchMenu() error {
|
||||
branch := gui.getSelectedBranch()
|
||||
if branch == nil {
|
||||
return nil
|
||||
@@ -408,27 +433,36 @@ func (gui *Gui) handleCreateResetToBranchMenu(g *gocui.Gui, v *gocui.View) error
|
||||
return gui.createResetMenu(branch.Name)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRenameBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleRenameBranch() 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.prompt(gui.Tr.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(newBranchName, commands.CheckoutOptions{Force: false}); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.NewBranchNamePrompt + " " + branch.Name + ":",
|
||||
initialContent: branch.Name,
|
||||
handleConfirm: func(newBranchName string) error {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.RenameBranch).RenameBranch(branch.Name, newBranchName); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
// need to find where the branch is now so that we can re-select it. That means we need to refetch the branches synchronously and then find our branch
|
||||
gui.refreshBranches()
|
||||
|
||||
// now that we've got our stuff again we need to find that branch and reselect it.
|
||||
for i, newBranch := range gui.State.Branches {
|
||||
if newBranch.Name == newBranchName {
|
||||
gui.State.Panels.Branches.SetSelectedLineIdx(i)
|
||||
if err := gui.State.Contexts.Branches.HandleRender(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -441,7 +475,6 @@ func (gui *Gui) handleRenameBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
return gui.ask(askOpts{
|
||||
|
||||
title: gui.Tr.LcRenameBranch,
|
||||
prompt: gui.Tr.RenameBranchWarning,
|
||||
handleConfirm: promptForNewName,
|
||||
@@ -456,7 +489,7 @@ func (gui *Gui) currentBranch() *models.Branch {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleNewBranchOffCurrentItem() error {
|
||||
context := gui.currentSideContext()
|
||||
context := gui.currentSideListContext()
|
||||
|
||||
item, ok := context.GetSelectedItem()
|
||||
if !ok {
|
||||
@@ -475,25 +508,62 @@ func (gui *Gui) handleNewBranchOffCurrentItem() error {
|
||||
// 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 gui.prompt(promptOpts{
|
||||
title: message,
|
||||
initialContent: prefilledName,
|
||||
handleConfirm: func(response string) error {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.CreateBranch).NewBranch(sanitizedBranchName(response), item.ID()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
gui.State.Panels.Branches.SelectedLineIdx = 0
|
||||
// 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)
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
if context.GetKey() != gui.State.Contexts.Branches.GetKey() {
|
||||
if err := gui.pushContext(gui.State.Contexts.Branches); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
gui.State.Panels.Branches.SelectedLineIdx = 0
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) getBranchNames() []string {
|
||||
result := make([]string, len(gui.State.Branches))
|
||||
|
||||
for i, branch := range gui.State.Branches {
|
||||
result[i] = branch.Name
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (gui *Gui) findBranchNameSuggestions(input string) []*types.Suggestion {
|
||||
branchNames := gui.getBranchNames()
|
||||
|
||||
matchingBranchNames := utils.FuzzySearch(sanitizedBranchName(input), branchNames)
|
||||
|
||||
suggestions := make([]*types.Suggestion, len(matchingBranchNames))
|
||||
for i, branchName := range matchingBranchNames {
|
||||
suggestions[i] = &types.Suggestion{
|
||||
Value: branchName,
|
||||
Label: utils.ColoredString(branchName, presentation.GetBranchColor(branchName)),
|
||||
}
|
||||
}
|
||||
|
||||
return suggestions
|
||||
}
|
||||
|
||||
// sanitizedBranchName will remove all spaces in favor of a dash "-" to meet
|
||||
// git's branch naming requirement.
|
||||
func sanitizedBranchName(input string) string {
|
||||
return strings.Replace(input, " ", "-", -1)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func (gui *Gui) handleCopyCommit() error {
|
||||
}
|
||||
|
||||
// get currently selected commit, add the sha to state.
|
||||
context := gui.currentSideContext()
|
||||
context := gui.currentSideListContext()
|
||||
if context == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -63,7 +63,7 @@ func (gui *Gui) cherryPickedCommitShaMap() map[string]bool {
|
||||
}
|
||||
|
||||
func (gui *Gui) commitsListForContext() []*models.Commit {
|
||||
context := gui.currentSideContext()
|
||||
context := gui.currentSideListContext()
|
||||
if context == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -104,7 +104,7 @@ func (gui *Gui) handleCopyCommitRange() error {
|
||||
}
|
||||
|
||||
// get currently selected commit, add the sha to state.
|
||||
context := gui.currentSideContext()
|
||||
context := gui.currentSideListContext()
|
||||
if context == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -148,7 +148,7 @@ func (gui *Gui) HandlePasteCommits() error {
|
||||
prompt: gui.Tr.SureCherryPick,
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.CherryPickingStatus, func() error {
|
||||
err := gui.GitCommand.CherryPickCommits(gui.State.Modes.CherryPicking.CherryPickedCommits)
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.CherryPick).CherryPickCommits(gui.State.Modes.CherryPicking.CherryPickedCommits)
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
},
|
||||
@@ -169,7 +169,7 @@ func (gui *Gui) exitCherryPickingMode() error {
|
||||
return gui.rerenderContextViewIfPresent(contextKey)
|
||||
}
|
||||
|
||||
func (gui *Gui) rerenderContextViewIfPresent(contextKey string) error {
|
||||
func (gui *Gui) rerenderContextViewIfPresent(contextKey ContextKey) error {
|
||||
if contextKey == "" {
|
||||
return nil
|
||||
}
|
||||
@@ -184,7 +184,7 @@ func (gui *Gui) rerenderContextViewIfPresent(contextKey string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if view.Context == contextKey {
|
||||
if ContextKey(view.Context) == contextKey {
|
||||
if err := context.HandleRender(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
185
pkg/gui/command_log_panel.go
Normal file
185
pkg/gui/command_log_panel.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/constants"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func (gui *Gui) GetOnRunCommand() func(entry oscommands.CmdLogEntry) {
|
||||
// closing over this so that nobody else can modify it
|
||||
currentSpan := ""
|
||||
|
||||
return func(entry oscommands.CmdLogEntry) {
|
||||
if gui.Views.Extras == nil {
|
||||
return
|
||||
}
|
||||
|
||||
gui.Views.Extras.Autoscroll = true
|
||||
|
||||
if entry.GetSpan() != currentSpan {
|
||||
fmt.Fprint(gui.Views.Extras, "\n"+utils.ColoredString(entry.GetSpan(), color.FgYellow))
|
||||
currentSpan = entry.GetSpan()
|
||||
}
|
||||
|
||||
clrAttr := theme.DefaultTextColor
|
||||
if !entry.GetCommandLine() {
|
||||
clrAttr = color.FgMagenta
|
||||
}
|
||||
gui.CmdLog = append(gui.CmdLog, entry.GetCmdStr())
|
||||
indentedCmdStr := " " + strings.Replace(entry.GetCmdStr(), "\n", "\n ", -1)
|
||||
fmt.Fprint(gui.Views.Extras, "\n"+utils.ColoredString(indentedCmdStr, clrAttr))
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) printCommandLogHeader() {
|
||||
introStr := fmt.Sprintf(
|
||||
gui.Tr.CommandLogHeader,
|
||||
gui.getKeyDisplay(gui.Config.GetUserConfig().Keybinding.Universal.ExtrasMenu),
|
||||
)
|
||||
fmt.Fprintln(gui.Views.Extras, utils.ColoredString(introStr, color.FgCyan))
|
||||
|
||||
if gui.Config.GetUserConfig().Gui.ShowRandomTip {
|
||||
fmt.Fprintf(
|
||||
gui.Views.Extras,
|
||||
"%s: %s",
|
||||
utils.ColoredString(gui.Tr.RandomTip, color.FgYellow),
|
||||
utils.ColoredString(gui.getRandomTip(), color.FgGreen),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) getRandomTip() string {
|
||||
config := gui.Config.GetUserConfig().Keybinding
|
||||
|
||||
formattedKey := func(key string) string {
|
||||
return gui.getKeyDisplay(key)
|
||||
}
|
||||
|
||||
tips := []string{
|
||||
// keybindings and lazygit-specific advice
|
||||
fmt.Sprintf(
|
||||
"To force push, press '%s' and then if the push is rejected you will be asked if you want to force push",
|
||||
formattedKey(config.Universal.PushFiles),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"To filter commits by path, press '%s'",
|
||||
formattedKey(config.Universal.FilteringMenu),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"To start an interactive rebase, press '%s' on a commit. You can always abort the rebase by pressing '%s' and selecting 'abort'",
|
||||
formattedKey(config.Universal.Edit),
|
||||
formattedKey(config.Universal.CreateRebaseOptionsMenu),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"In flat file view, merge conflicts are sorted to the top. To switch to flat file view press '%s'",
|
||||
formattedKey(config.Files.ToggleTreeView),
|
||||
),
|
||||
"If you want to learn Go and can think of ways to improve lazygit, join the team! Click 'Ask Question' and express your interest",
|
||||
fmt.Sprintf(
|
||||
"If you press '%s'/'%s' you can undo/redo your changes. Be wary though, this only applies to branches/commits, so only do this if your worktree is clear.\nDocs: %s",
|
||||
formattedKey(config.Universal.Undo),
|
||||
formattedKey(config.Universal.Redo),
|
||||
constants.Links.Docs.Undoing,
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"to hard reset onto your current upstream branch, press '%s' in the files panel",
|
||||
formattedKey(config.Files.ViewResetOptions),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"To push a tag, navigate to the tag in the tags tab and press '%s'",
|
||||
formattedKey(config.Branches.PushTag),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"You can view the individual files of a stash entry by pressing '%s'",
|
||||
formattedKey(config.Universal.GoInto),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"You can diff two commits by pressing '%s' one one commit and then navigating to the other. You can then press '%s' to view the files of the diff",
|
||||
formattedKey(config.Universal.DiffingMenu),
|
||||
formattedKey(config.Universal.GoInto),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"press '%s' on a commit to drop it (delete it)",
|
||||
formattedKey(config.Universal.Remove),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"If you need to pull out the big guns to resolve merge conflicts, you can press '%s' in the files panel to open 'git mergetool'",
|
||||
formattedKey(config.Files.OpenMergeTool),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"To revert a commit, press '%s' on that commit",
|
||||
formattedKey(config.Commits.RevertCommit),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"To escape a mode, for example cherry-picking, patch-building, diffing, or filtering mode, you can just spam the '%s' button. Unless of course you have `quitOnTopLevelReturn` enabled in your config",
|
||||
formattedKey(config.Universal.Return),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"To search for a string in your panel, press '%s'",
|
||||
formattedKey(config.Universal.StartSearch),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"You can page through the items of a panel using '%s' and '%s'",
|
||||
formattedKey(config.Universal.PrevPage),
|
||||
formattedKey(config.Universal.NextPage),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"You can jump to the top/bottom of a panel using '%s' and '%s'",
|
||||
formattedKey(config.Universal.GotoTop),
|
||||
formattedKey(config.Universal.GotoBottom),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"To collapse/expand a directory, press '%s'",
|
||||
formattedKey(config.Universal.GoInto),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"You can append your staged changes to an older commit by pressing '%s' on that commit",
|
||||
formattedKey(config.Commits.AmendToCommit),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"You can amend the last commit with your new file changes by pressing '%s' in the files panel",
|
||||
formattedKey(config.Files.AmendLastCommit),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"You can now navigate the side panels with '%s' and '%s'",
|
||||
formattedKey(config.Universal.NextBlockAlt2),
|
||||
formattedKey(config.Universal.PrevBlockAlt2),
|
||||
),
|
||||
|
||||
"You can use lazygit with a bare repo by passing the --git-dir and --work-tree arguments as you would for the git CLI",
|
||||
|
||||
// general advice
|
||||
"`git commit` is really just the programmer equivalent of saving your game. Always do it before embarking on an ambitious change!",
|
||||
"Try to separate commits that refactor code from commits that add new functionality: if they're squashed into the one commit, it can be hard to spot what's new.",
|
||||
"If you ever want to experiment, it's easy to create a new branch off your current one and go nuts, then delete it afterwards",
|
||||
"Always read through the diff of your changes before assigning somebody to review your code. Better for you to catch any silly mistakes than your colleagues!",
|
||||
"If something goes wrong, you can always checkout a commit from your reflog to return to an earlier state",
|
||||
"The stash is a good place to save snippets of code that you always find yourself adding when debugging.",
|
||||
|
||||
// links
|
||||
fmt.Sprintf(
|
||||
"If you want a git diff with syntax colouring, check out lazygit's integration with delta:\n%s",
|
||||
constants.Links.Docs.CustomPagers,
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"You can build your own custom menus and commands to run from within lazygit. For examples see:\n%s",
|
||||
constants.Links.Docs.CustomCommands,
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"If you ever find a bug, do not hesistate to raise an issue on the repo:\n%s",
|
||||
constants.Links.Issues,
|
||||
),
|
||||
}
|
||||
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
randomIndex := rand.Intn(len(tips))
|
||||
return tips[randomIndex]
|
||||
}
|
||||
@@ -1,34 +1,51 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
||||
)
|
||||
|
||||
func (gui *Gui) getSelectedCommitFile() *models.CommitFile {
|
||||
func (gui *Gui) getSelectedCommitFileNode() *filetree.CommitFileNode {
|
||||
selectedLine := gui.State.Panels.CommitFiles.SelectedLineIdx
|
||||
if selectedLine == -1 || selectedLine > len(gui.State.CommitFiles)-1 {
|
||||
if selectedLine == -1 || selectedLine > gui.State.CommitFileManager.GetItemsLength()-1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.State.CommitFiles[selectedLine]
|
||||
return gui.State.CommitFileManager.GetItemAtIndex(selectedLine)
|
||||
}
|
||||
|
||||
func (gui *Gui) getSelectedCommitFile() *models.CommitFile {
|
||||
node := gui.getSelectedCommitFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
return node.File
|
||||
}
|
||||
|
||||
func (gui *Gui) getSelectedCommitFilePath() string {
|
||||
node := gui.getSelectedCommitFileNode()
|
||||
if node == nil {
|
||||
return ""
|
||||
}
|
||||
return node.GetPath()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitFileSelect() error {
|
||||
gui.escapeLineByLinePanel()
|
||||
|
||||
commitFile := gui.getSelectedCommitFile()
|
||||
if commitFile == nil {
|
||||
node := gui.getSelectedCommitFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
to := commitFile.Parent
|
||||
to := gui.State.CommitFileManager.GetParent()
|
||||
from, reverse := gui.getFromAndReverseArgsForDiff(to)
|
||||
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.ShowFileDiffCmdStr(from, to, reverse, commitFile.Name, false),
|
||||
gui.GitCommand.ShowFileDiffCmdStr(from, to, reverse, node.GetPath(), false),
|
||||
)
|
||||
task := gui.createRunPtyTask(cmd)
|
||||
task := NewRunPtyTask(cmd)
|
||||
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
main: &viewUpdateOpts{
|
||||
@@ -39,32 +56,32 @@ func (gui *Gui) handleCommitFileSelect() error {
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCheckoutCommitFile(g *gocui.Gui, v *gocui.View) error {
|
||||
file := gui.getSelectedCommitFile()
|
||||
if file == nil {
|
||||
func (gui *Gui) handleCheckoutCommitFile() error {
|
||||
node := gui.getSelectedCommitFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := gui.GitCommand.CheckoutFile(file.Parent, file.Name); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.CheckoutFile).CheckoutFile(gui.State.CommitFileManager.GetParent(), node.GetPath()); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleDiscardOldFileChange(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleDiscardOldFileChange() error {
|
||||
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
fileName := gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLineIdx].Name
|
||||
fileName := gui.getSelectedCommitFileName()
|
||||
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.DiscardFileChangesTitle,
|
||||
prompt: gui.Tr.DiscardFileChangesPrompt,
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error {
|
||||
if err := gui.GitCommand.DiscardOldFileChanges(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, fileName); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.DiscardOldFileChange).DiscardOldFileChanges(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, fileName); err != nil {
|
||||
if err := gui.handleGenericMergeCommandResult(err); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -77,43 +94,50 @@ func (gui *Gui) handleDiscardOldFileChange(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshCommitFilesView() error {
|
||||
if err := gui.handleRefreshPatchBuildingPanel(-1); err != nil {
|
||||
return err
|
||||
currentSideContext := gui.currentSideContext()
|
||||
if currentSideContext.GetKey() == COMMIT_FILES_CONTEXT_KEY || currentSideContext.GetKey() == BRANCH_COMMITS_CONTEXT_KEY {
|
||||
if err := gui.handleRefreshPatchBuildingPanel(-1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
to := gui.State.Panels.CommitFiles.refName
|
||||
from, reverse := gui.getFromAndReverseArgsForDiff(to)
|
||||
|
||||
files, err := gui.GitCommand.GetFilesInDiff(from, to, reverse, gui.GitCommand.PatchManager)
|
||||
files, err := gui.GitCommand.GetFilesInDiff(from, to, reverse)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
gui.State.CommitFiles = files
|
||||
gui.State.CommitFileManager.SetFiles(files, to)
|
||||
|
||||
return gui.postRefreshUpdate(gui.Contexts.CommitFiles.Context)
|
||||
return gui.postRefreshUpdate(gui.State.Contexts.CommitFiles)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleOpenOldCommitFile(g *gocui.Gui, v *gocui.View) error {
|
||||
file := gui.getSelectedCommitFile()
|
||||
if file == nil {
|
||||
func (gui *Gui) handleOpenOldCommitFile() error {
|
||||
node := gui.getSelectedCommitFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.openFile(file.Name)
|
||||
return gui.openFile(node.GetPath())
|
||||
}
|
||||
|
||||
func (gui *Gui) handleEditCommitFile(g *gocui.Gui, v *gocui.View) error {
|
||||
file := gui.getSelectedCommitFile()
|
||||
if file == nil {
|
||||
func (gui *Gui) handleEditCommitFile() error {
|
||||
node := gui.getSelectedCommitFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.editFile(file.Name)
|
||||
if node.File == nil {
|
||||
return gui.createErrorPanel(gui.Tr.ErrCannotEditDirectory)
|
||||
}
|
||||
|
||||
return gui.editFile(node.GetPath())
|
||||
}
|
||||
|
||||
func (gui *Gui) handleToggleFileForPatch(g *gocui.Gui, v *gocui.View) error {
|
||||
commitFile := gui.getSelectedCommitFile()
|
||||
if commitFile == nil {
|
||||
func (gui *Gui) handleToggleFileForPatch() error {
|
||||
node := gui.getSelectedCommitFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -124,18 +148,32 @@ func (gui *Gui) handleToggleFileForPatch(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := gui.GitCommand.PatchManager.ToggleFileWhole(commitFile.Name); err != nil {
|
||||
return err
|
||||
// if there is any file that hasn't been fully added we'll fully add everything,
|
||||
// otherwise we'll remove everything
|
||||
adding := node.AnyFile(func(file *models.CommitFile) bool {
|
||||
return gui.GitCommand.PatchManager.GetFileStatus(file.Name, gui.State.CommitFileManager.GetParent()) != patch.WHOLE
|
||||
})
|
||||
|
||||
err := node.ForEachFile(func(file *models.CommitFile) error {
|
||||
if adding {
|
||||
return gui.GitCommand.PatchManager.AddFileWhole(file.Name)
|
||||
} else {
|
||||
return gui.GitCommand.PatchManager.RemoveFile(file.Name)
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
if gui.GitCommand.PatchManager.IsEmpty() {
|
||||
gui.GitCommand.PatchManager.Reset()
|
||||
}
|
||||
|
||||
return gui.refreshCommitFilesView()
|
||||
return gui.postRefreshUpdate(gui.State.Contexts.CommitFiles)
|
||||
}
|
||||
|
||||
if gui.GitCommand.PatchManager.Active() && gui.GitCommand.PatchManager.To != commitFile.Parent {
|
||||
if gui.GitCommand.PatchManager.Active() && gui.GitCommand.PatchManager.To != gui.State.CommitFileManager.GetParent() {
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.DiscardPatch,
|
||||
prompt: gui.Tr.DiscardPatchConfirm,
|
||||
@@ -159,16 +197,20 @@ func (gui *Gui) startPatchManager() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleEnterCommitFile(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleEnterCommitFile() error {
|
||||
return gui.enterCommitFile(-1)
|
||||
}
|
||||
|
||||
func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
|
||||
commitFile := gui.getSelectedCommitFile()
|
||||
if commitFile == nil {
|
||||
node := gui.getSelectedCommitFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if node.File == nil {
|
||||
return gui.handleToggleCommitFileDirCollapsed()
|
||||
}
|
||||
|
||||
enterTheFile := func(selectedLineIdx int) error {
|
||||
if !gui.GitCommand.PatchManager.Active() {
|
||||
if err := gui.startPatchManager(); err != nil {
|
||||
@@ -176,13 +218,13 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := gui.switchContext(gui.Contexts.PatchBuilding.Context); err != nil {
|
||||
if err := gui.pushContext(gui.State.Contexts.PatchBuilding); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.handleRefreshPatchBuildingPanel(selectedLineIdx)
|
||||
}
|
||||
|
||||
if gui.GitCommand.PatchManager.Active() && gui.GitCommand.PatchManager.To != commitFile.Parent {
|
||||
if gui.GitCommand.PatchManager.Active() && gui.GitCommand.PatchManager.To != gui.State.CommitFileManager.GetParent() {
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.DiscardPatch,
|
||||
prompt: gui.Tr.DiscardPatchConfirm,
|
||||
@@ -192,7 +234,7 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
|
||||
return enterTheFile(selectedLineIdx)
|
||||
},
|
||||
handleClose: func() error {
|
||||
return gui.switchContext(gui.Contexts.CommitFiles.Context)
|
||||
return gui.pushContext(gui.State.Contexts.CommitFiles)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -200,20 +242,60 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
|
||||
return enterTheFile(selectedLineIdx)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleToggleCommitFileDirCollapsed() error {
|
||||
node := gui.getSelectedCommitFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.State.CommitFileManager.ToggleCollapsed(node.GetPath())
|
||||
|
||||
if err := gui.postRefreshUpdate(gui.State.Contexts.CommitFiles); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) switchToCommitFilesContext(refName string, canRebase bool, context Context, windowName string) error {
|
||||
// sometimes the commitFiles view is already shown in another window, so we need to ensure that window
|
||||
// no longer considers the commitFiles view as its main view.
|
||||
gui.resetWindowForView("commitFiles")
|
||||
gui.resetWindowForView(gui.Views.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)
|
||||
gui.State.Contexts.CommitFiles.SetParentContext(context)
|
||||
gui.State.Contexts.CommitFiles.SetWindowName(windowName)
|
||||
|
||||
if err := gui.refreshCommitFilesView(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.switchContext(gui.Contexts.CommitFiles.Context)
|
||||
return gui.pushContext(gui.State.Contexts.CommitFiles)
|
||||
}
|
||||
|
||||
// NOTE: this is very similar to handleToggleFileTreeView, could be DRY'd with generics
|
||||
func (gui *Gui) handleToggleCommitFileTreeView() error {
|
||||
path := gui.getSelectedCommitFilePath()
|
||||
|
||||
gui.State.CommitFileManager.ToggleShowTree()
|
||||
|
||||
// find that same node in the new format and move the cursor to it
|
||||
if path != "" {
|
||||
gui.State.CommitFileManager.ExpandToPath(path)
|
||||
index, found := gui.State.CommitFileManager.GetIndexForPath(path)
|
||||
if found {
|
||||
gui.State.Contexts.CommitFiles.GetPanelState().SetSelectedLineIdx(index)
|
||||
}
|
||||
}
|
||||
|
||||
if err := gui.State.Contexts.CommitFiles.HandleRender(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.State.Contexts.CommitFiles.HandleFocus(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,33 +1,16 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// runSyncOrAsyncCommand takes the output of a command that may have returned
|
||||
// either no error, an error, or a subprocess to execute, and if a subprocess
|
||||
// needs to be set on the gui object, it does so, and then returns the error
|
||||
// the bool returned tells us whether the calling code should continue
|
||||
func (gui *Gui) runSyncOrAsyncCommand(sub *exec.Cmd, err error) (bool, error) {
|
||||
if err != nil {
|
||||
if err != gui.Errors.ErrSubProcess {
|
||||
return false, gui.surfaceError(err)
|
||||
}
|
||||
}
|
||||
if sub != nil {
|
||||
gui.SubProcess = sub
|
||||
return false, gui.Errors.ErrSubProcess
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
|
||||
message := gui.trimmedContent(v)
|
||||
func (gui *Gui) handleCommitConfirm() error {
|
||||
message := gui.trimmedContent(gui.Views.CommitMessage)
|
||||
if message == "" {
|
||||
return gui.createErrorPanel(gui.Tr.CommitWithoutMessageErr)
|
||||
}
|
||||
@@ -36,20 +19,17 @@ func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
|
||||
if skipHookPrefix != "" && strings.HasPrefix(message, skipHookPrefix) {
|
||||
flags = "--no-verify"
|
||||
}
|
||||
ok, err := gui.runSyncOrAsyncCommand(gui.GitCommand.Commit(message, flags))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.clearEditorView(v)
|
||||
_ = gui.returnFromContext()
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
cmdStr := gui.GitCommand.CommitCmdStr(message, flags)
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(cmdStr, gui.Tr.Spans.Commit, true))
|
||||
return gui.withGpgHandling(cmdStr, gui.Tr.CommittingStatus, func() error {
|
||||
_ = gui.returnFromContext()
|
||||
gui.clearEditorView(gui.Views.CommitMessage)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitClose(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCommitClose() error {
|
||||
return gui.returnFromContext()
|
||||
}
|
||||
|
||||
@@ -57,13 +37,13 @@ func (gui *Gui) handleCommitMessageFocused() error {
|
||||
message := utils.ResolvePlaceholderString(
|
||||
gui.Tr.CommitMessageConfirm,
|
||||
map[string]string{
|
||||
"keyBindClose": "esc",
|
||||
"keyBindConfirm": "enter",
|
||||
"keyBindNewLine": "tab",
|
||||
"keyBindClose": gui.getKeyDisplay(gui.Config.GetUserConfig().Keybinding.Universal.Return),
|
||||
"keyBindConfirm": gui.getKeyDisplay(gui.Config.GetUserConfig().Keybinding.Universal.Confirm),
|
||||
"keyBindNewLine": gui.getKeyDisplay(gui.Config.GetUserConfig().Keybinding.Universal.AppendNewline),
|
||||
},
|
||||
)
|
||||
|
||||
gui.renderString("options", message)
|
||||
gui.renderString(gui.Views.Options, message)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -76,46 +56,6 @@ func (gui *Gui) RenderCommitLength() {
|
||||
if !gui.Config.GetUserConfig().Gui.CommitLength.Show {
|
||||
return
|
||||
}
|
||||
v := gui.getCommitMessageView()
|
||||
v.Subtitle = gui.getBufferLength(v)
|
||||
}
|
||||
|
||||
// we've just copy+pasted the editor from gocui to here so that we can also re-
|
||||
// render the commit message length on each keypress
|
||||
func (gui *Gui) commitMessageEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
|
||||
newlineKey, ok := gui.getKey(gui.Config.GetUserConfig().Keybinding.Universal.AppendNewline).(gocui.Key)
|
||||
if !ok {
|
||||
newlineKey = gocui.KeyTab
|
||||
}
|
||||
|
||||
switch {
|
||||
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
|
||||
v.EditDelete(true)
|
||||
case key == gocui.KeyDelete:
|
||||
v.EditDelete(false)
|
||||
case key == gocui.KeyArrowDown:
|
||||
v.MoveCursor(0, 1, false)
|
||||
case key == gocui.KeyArrowUp:
|
||||
v.MoveCursor(0, -1, false)
|
||||
case key == gocui.KeyArrowLeft:
|
||||
v.MoveCursor(-1, 0, false)
|
||||
case key == gocui.KeyArrowRight:
|
||||
v.MoveCursor(1, 0, false)
|
||||
case key == newlineKey:
|
||||
v.EditNewLine()
|
||||
case key == gocui.KeySpace:
|
||||
v.EditWrite(' ')
|
||||
case key == gocui.KeyInsert:
|
||||
v.Overwrite = !v.Overwrite
|
||||
case key == gocui.KeyCtrlU:
|
||||
v.EditDeleteToStartOfLine()
|
||||
case key == gocui.KeyCtrlA:
|
||||
v.EditGotoToStartOfLine()
|
||||
case key == gocui.KeyCtrlE:
|
||||
v.EditGotoToEndOfLine()
|
||||
default:
|
||||
v.EditWrite(ch)
|
||||
}
|
||||
|
||||
gui.RenderCommitLength()
|
||||
|
||||
gui.Views.CommitMessage.Subtitle = gui.getBufferLength(gui.Views.CommitMessage)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -13,7 +14,7 @@ import (
|
||||
|
||||
func (gui *Gui) getSelectedLocalCommit() *models.Commit {
|
||||
selectedLine := gui.State.Panels.Commits.SelectedLineIdx
|
||||
if selectedLine == -1 {
|
||||
if selectedLine == -1 || selectedLine > len(gui.State.Commits)-1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -36,12 +37,12 @@ func (gui *Gui) handleCommitSelect() error {
|
||||
var task updateTask
|
||||
commit := gui.getSelectedLocalCommit()
|
||||
if commit == nil {
|
||||
task = gui.createRenderStringTask(gui.Tr.NoCommitsThisBranch)
|
||||
task = NewRenderStringTask(gui.Tr.NoCommitsThisBranch)
|
||||
} else {
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.Modes.Filtering.Path),
|
||||
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.Modes.Filtering.GetPath()),
|
||||
)
|
||||
task = gui.createRunPtyTask(cmd)
|
||||
task = NewRunPtyTask(cmd)
|
||||
}
|
||||
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
@@ -87,7 +88,7 @@ func (gui *Gui) refreshCommits() error {
|
||||
|
||||
go utils.Safe(func() {
|
||||
_ = gui.refreshCommitsWithLimit()
|
||||
context, ok := gui.Contexts.CommitFiles.Context.GetParentContext()
|
||||
context, ok := gui.State.Contexts.CommitFiles.GetParentContext()
|
||||
if ok && context.GetKey() == BRANCH_COMMITS_CONTEXT_KEY {
|
||||
// This makes sense when we've e.g. just amended a commit, meaning we get a new commit SHA at the same position.
|
||||
// However if we've just added a brand new commit, it pushes the list down by one and so we would end up
|
||||
@@ -118,7 +119,7 @@ func (gui *Gui) refreshCommitsWithLimit() error {
|
||||
commits, err := builder.GetCommits(
|
||||
commands.GetCommitsOptions{
|
||||
Limit: gui.State.Panels.Commits.LimitCommits,
|
||||
FilterPath: gui.State.Modes.Filtering.Path,
|
||||
FilterPath: gui.State.Modes.Filtering.GetPath(),
|
||||
IncludeRebaseCommits: true,
|
||||
RefName: "HEAD",
|
||||
},
|
||||
@@ -128,7 +129,7 @@ func (gui *Gui) refreshCommitsWithLimit() error {
|
||||
}
|
||||
gui.State.Commits = commits
|
||||
|
||||
return gui.postRefreshUpdate(gui.Contexts.BranchCommits.Context)
|
||||
return gui.postRefreshUpdate(gui.State.Contexts.BranchCommits)
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshRebaseCommits() error {
|
||||
@@ -143,12 +144,12 @@ func (gui *Gui) refreshRebaseCommits() error {
|
||||
}
|
||||
gui.State.Commits = updatedCommits
|
||||
|
||||
return gui.postRefreshUpdate(gui.Contexts.BranchCommits.Context)
|
||||
return gui.postRefreshUpdate(gui.State.Contexts.BranchCommits)
|
||||
}
|
||||
|
||||
// specific functions
|
||||
|
||||
func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCommitSquashDown() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
@@ -170,14 +171,14 @@ func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
|
||||
prompt: gui.Tr.SureSquashThisCommit,
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.SquashingStatus, func() error {
|
||||
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "squash")
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.SquashCommitDown).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 {
|
||||
func (gui *Gui) handleCommitFixup() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
@@ -199,14 +200,14 @@ func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
|
||||
prompt: gui.Tr.SureFixupThisCommit,
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.FixingStatus, func() error {
|
||||
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "fixup")
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.FixupCommit).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 {
|
||||
func (gui *Gui) handleRenameCommit() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
@@ -233,16 +234,20 @@ func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.prompt(gui.Tr.LcRenameCommit, message, func(response string) error {
|
||||
if err := gui.GitCommand.RenameCommit(response); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.LcRenameCommit,
|
||||
initialContent: message,
|
||||
handleConfirm: func(response string) error {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.RewordCommit).RenameCommit(response); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleRenameCommitEditor() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
@@ -255,13 +260,12 @@ 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.SelectedLineIdx)
|
||||
subProcess, err := gui.GitCommand.WithSpan(gui.Tr.Spans.RewordCommit).RewordCommit(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
if subProcess != nil {
|
||||
gui.SubProcess = subProcess
|
||||
return gui.Errors.ErrSubProcess
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(subProcess)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -284,6 +288,12 @@ func (gui *Gui) handleMidRebaseCommand(action string) (bool, error) {
|
||||
return true, gui.createErrorPanel(gui.Tr.LcRewordNotSupported)
|
||||
}
|
||||
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(
|
||||
fmt.Sprintf("Updating rebase action of commit %s to '%s'", selectedCommit.ShortSha(), action),
|
||||
"Update rebase TODO",
|
||||
false,
|
||||
))
|
||||
|
||||
if err := gui.GitCommand.EditRebaseTodo(gui.State.Panels.Commits.SelectedLineIdx, action); err != nil {
|
||||
return false, gui.surfaceError(err)
|
||||
}
|
||||
@@ -291,7 +301,7 @@ func (gui *Gui) handleMidRebaseCommand(action string) (bool, error) {
|
||||
return true, gui.refreshRebaseCommits()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitDelete(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCommitDelete() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
@@ -309,24 +319,35 @@ func (gui *Gui) handleCommitDelete(g *gocui.Gui, v *gocui.View) error {
|
||||
prompt: gui.Tr.DeleteCommitPrompt,
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.DeletingStatus, func() error {
|
||||
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "drop")
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.DropCommit).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 {
|
||||
func (gui *Gui) handleCommitMoveDown() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
span := gui.Tr.Spans.MoveCommitDown
|
||||
|
||||
index := gui.State.Panels.Commits.SelectedLineIdx
|
||||
selectedCommit := gui.State.Commits[index]
|
||||
if selectedCommit.Status == "rebasing" {
|
||||
if gui.State.Commits[index+1].Status != "rebasing" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// logging directly here because MoveTodoDown doesn't have enough information
|
||||
// to provide a useful log
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(
|
||||
fmt.Sprintf("Moving commit %s down", selectedCommit.ShortSha()),
|
||||
span,
|
||||
false,
|
||||
))
|
||||
|
||||
if err := gui.GitCommand.MoveTodoDown(index); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
@@ -335,7 +356,7 @@ func (gui *Gui) handleCommitMoveDown(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.MovingStatus, func() error {
|
||||
err := gui.GitCommand.MoveCommitDown(gui.State.Commits, index)
|
||||
err := gui.GitCommand.WithSpan(span).MoveCommitDown(gui.State.Commits, index)
|
||||
if err == nil {
|
||||
gui.State.Panels.Commits.SelectedLineIdx++
|
||||
}
|
||||
@@ -343,7 +364,7 @@ func (gui *Gui) handleCommitMoveDown(g *gocui.Gui, v *gocui.View) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitMoveUp(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCommitMoveUp() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
@@ -352,8 +373,19 @@ func (gui *Gui) handleCommitMoveUp(g *gocui.Gui, v *gocui.View) error {
|
||||
if index == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
span := gui.Tr.Spans.MoveCommitUp
|
||||
|
||||
selectedCommit := gui.State.Commits[index]
|
||||
if selectedCommit.Status == "rebasing" {
|
||||
// logging directly here because MoveTodoDown doesn't have enough information
|
||||
// to provide a useful log
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(
|
||||
fmt.Sprintf("Moving commit %s up", selectedCommit.ShortSha()),
|
||||
span,
|
||||
false,
|
||||
))
|
||||
|
||||
if err := gui.GitCommand.MoveTodoDown(index - 1); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
@@ -362,7 +394,7 @@ func (gui *Gui) handleCommitMoveUp(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.MovingStatus, func() error {
|
||||
err := gui.GitCommand.MoveCommitDown(gui.State.Commits, index-1)
|
||||
err := gui.GitCommand.WithSpan(span).MoveCommitDown(gui.State.Commits, index-1)
|
||||
if err == nil {
|
||||
gui.State.Panels.Commits.SelectedLineIdx--
|
||||
}
|
||||
@@ -370,7 +402,7 @@ func (gui *Gui) handleCommitMoveUp(g *gocui.Gui, v *gocui.View) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitEdit(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCommitEdit() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
@@ -384,12 +416,12 @@ func (gui *Gui) handleCommitEdit(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error {
|
||||
err = gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "edit")
|
||||
err = gui.GitCommand.WithSpan(gui.Tr.Spans.EditCommit).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 {
|
||||
func (gui *Gui) handleCommitAmendTo() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
@@ -399,14 +431,14 @@ func (gui *Gui) handleCommitAmendTo(g *gocui.Gui, v *gocui.View) error {
|
||||
prompt: gui.Tr.AmendCommitPrompt,
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.AmendingStatus, func() error {
|
||||
err := gui.GitCommand.AmendTo(gui.State.Commits[gui.State.Panels.Commits.SelectedLineIdx].Sha)
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.AmendCommit).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 {
|
||||
func (gui *Gui) handleCommitPick() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
@@ -421,19 +453,19 @@ func (gui *Gui) handleCommitPick(g *gocui.Gui, v *gocui.View) error {
|
||||
|
||||
// at this point we aren't actually rebasing so we will interpret this as an
|
||||
// attempt to pull. We might revoke this later after enabling configurable keybindings
|
||||
return gui.handlePullFiles(g, v)
|
||||
return gui.handlePullFiles()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitRevert(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCommitRevert() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.GitCommand.Revert(gui.State.Commits[gui.State.Panels.Commits.SelectedLineIdx].Sha); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.RevertCommit).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}})
|
||||
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI, scope: []RefreshableView{COMMITS, BRANCHES}})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleViewCommitFiles() error {
|
||||
@@ -442,23 +474,10 @@ func (gui *Gui) handleViewCommitFiles() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.switchToCommitFilesContext(commit.Sha, true, gui.Contexts.BranchCommits.Context, "commits")
|
||||
return gui.switchToCommitFilesContext(commit.Sha, true, gui.State.Contexts.BranchCommits, "commits")
|
||||
}
|
||||
|
||||
func (gui *Gui) hasCommit(commits []*models.Commit, target string) (int, bool) {
|
||||
for idx, commit := range commits {
|
||||
if commit.Sha == target {
|
||||
return idx, true
|
||||
}
|
||||
}
|
||||
return -1, false
|
||||
}
|
||||
|
||||
func (gui *Gui) unchooseCommit(commits []*models.Commit, i int) []*models.Commit {
|
||||
return append(commits[:i], commits[i+1:]...)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateFixupCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCreateFixupCommit() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
@@ -479,7 +498,7 @@ func (gui *Gui) handleCreateFixupCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
title: gui.Tr.CreateFixupCommit,
|
||||
prompt: prompt,
|
||||
handleConfirm: func() error {
|
||||
if err := gui.GitCommand.CreateFixupCommit(commit.Sha); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.CreateFixupCommit).CreateFixupCommit(commit.Sha); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
@@ -488,7 +507,7 @@ func (gui *Gui) handleCreateFixupCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSquashAllAboveFixupCommits(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleSquashAllAboveFixupCommits() error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
@@ -510,14 +529,14 @@ func (gui *Gui) handleSquashAllAboveFixupCommits(g *gocui.Gui, v *gocui.View) er
|
||||
prompt: prompt,
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.SquashingStatus, func() error {
|
||||
err := gui.GitCommand.SquashAllAboveFixupCommits(commit.Sha)
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.SquashAllAboveFixupCommits).SquashAllAboveFixupCommits(commit.Sha)
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleTagCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleTagCommit() 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
|
||||
|
||||
@@ -530,15 +549,18 @@ func (gui *Gui) handleTagCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateLightweightTag(commitSha string) error {
|
||||
return gui.prompt(gui.Tr.TagNameTitle, "", func(response string) error {
|
||||
if err := gui.GitCommand.CreateLightweightTag(response, commitSha); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{COMMITS, TAGS}})
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.TagNameTitle,
|
||||
handleConfirm: func(response string) error {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.CreateLightweightTag).CreateLightweightTag(response, commitSha); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{COMMITS, TAGS}})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCheckoutCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCheckoutCommit() error {
|
||||
commit := gui.getSelectedLocalCommit()
|
||||
if commit == nil {
|
||||
return nil
|
||||
@@ -548,12 +570,12 @@ func (gui *Gui) handleCheckoutCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
title: gui.Tr.LcCheckoutCommit,
|
||||
prompt: gui.Tr.SureCheckoutThisCommit,
|
||||
handleConfirm: func() error {
|
||||
return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{})
|
||||
return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{span: gui.Tr.Spans.CheckoutCommit})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateCommitResetMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCreateCommitResetMenu() error {
|
||||
commit := gui.getSelectedLocalCommit()
|
||||
if commit == nil {
|
||||
return gui.createErrorPanel(gui.Tr.NoCommitsThisBranch)
|
||||
@@ -562,30 +584,30 @@ func (gui *Gui) handleCreateCommitResetMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.createResetMenu(commit.Sha)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleOpenSearchForCommitsPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleOpenSearchForCommitsPanel(_viewName string) error {
|
||||
// we usually lazyload these commits but now that we're searching we need to load them now
|
||||
if gui.State.Panels.Commits.LimitCommits {
|
||||
gui.State.Panels.Commits.LimitCommits = false
|
||||
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{COMMITS}}); err != nil {
|
||||
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{COMMITS}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return gui.handleOpenSearch(gui.g, v)
|
||||
return gui.handleOpenSearch("commits")
|
||||
}
|
||||
|
||||
func (gui *Gui) handleGotoBottomForCommitsPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleGotoBottomForCommitsPanel() error {
|
||||
// we usually lazyload these commits but now that we're searching we need to load them now
|
||||
if gui.State.Panels.Commits.LimitCommits {
|
||||
gui.State.Panels.Commits.LimitCommits = false
|
||||
if err := gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []int{COMMITS}}); err != nil {
|
||||
if err := gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []RefreshableView{COMMITS}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, context := range gui.getListContexts() {
|
||||
if context.ViewName == "commits" {
|
||||
return context.handleGotoBottom(g, v)
|
||||
return context.handleGotoBottom()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -602,5 +624,12 @@ func (gui *Gui) handleCopySelectedCommitMessageToClipboard() error {
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.OSCommand.CopyToClipboard(message)
|
||||
|
||||
if err := gui.OSCommand.WithSpan(gui.Tr.Spans.CopyCommitMessageToClipboard).CopyToClipboard(message); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
gui.raiseToast(gui.Tr.CommitMessageCopiedToClipboard)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@ package gui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
@@ -27,6 +27,8 @@ type createPopupPanelOpts struct {
|
||||
|
||||
// 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
|
||||
|
||||
findSuggestionsFunc func(string) []*types.Suggestion
|
||||
}
|
||||
|
||||
type askOpts struct {
|
||||
@@ -35,13 +37,14 @@ type askOpts struct {
|
||||
handleConfirm func() error
|
||||
handleClose func() error
|
||||
handlersManageFocus bool
|
||||
findSuggestionsFunc func(string) []*types.Suggestion
|
||||
}
|
||||
|
||||
func (gui *Gui) createLoaderPanel(currentView *gocui.View, prompt string) error {
|
||||
return gui.createPopupPanel(createPopupPanelOpts{
|
||||
prompt: prompt,
|
||||
hasLoader: true,
|
||||
})
|
||||
type promptOpts struct {
|
||||
title string
|
||||
initialContent string
|
||||
handleConfirm func(string) error
|
||||
findSuggestionsFunc func(string) []*types.Suggestion
|
||||
}
|
||||
|
||||
func (gui *Gui) ask(opts askOpts) error {
|
||||
@@ -51,20 +54,29 @@ func (gui *Gui) ask(opts askOpts) error {
|
||||
handleConfirm: opts.handleConfirm,
|
||||
handleClose: opts.handleClose,
|
||||
handlersManageFocus: opts.handlersManageFocus,
|
||||
findSuggestionsFunc: opts.findSuggestionsFunc,
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) prompt(title string, initialContent string, handleConfirm func(string) error) error {
|
||||
func (gui *Gui) prompt(opts promptOpts) error {
|
||||
return gui.createPopupPanel(createPopupPanelOpts{
|
||||
title: title,
|
||||
prompt: initialContent,
|
||||
title: opts.title,
|
||||
prompt: opts.initialContent,
|
||||
editable: true,
|
||||
handleConfirmPrompt: handleConfirm,
|
||||
handleConfirmPrompt: opts.handleConfirm,
|
||||
findSuggestionsFunc: opts.findSuggestionsFunc,
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) wrappedConfirmationFunction(handlersManageFocus bool, function func() error) func(*gocui.Gui, *gocui.View) error {
|
||||
return func(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) createLoaderPanel(prompt string) error {
|
||||
return gui.createPopupPanel(createPopupPanelOpts{
|
||||
prompt: prompt,
|
||||
hasLoader: true,
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) wrappedConfirmationFunction(handlersManageFocus bool, function func() error) func() error {
|
||||
return func() error {
|
||||
if function != nil {
|
||||
if err := function(); err != nil {
|
||||
return err
|
||||
@@ -79,10 +91,10 @@ func (gui *Gui) wrappedConfirmationFunction(handlersManageFocus bool, function f
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
func (gui *Gui) wrappedPromptConfirmationFunction(handlersManageFocus bool, function func(string) error, getResponse func() string) func() error {
|
||||
return func() error {
|
||||
if function != nil {
|
||||
if err := function(v.Buffer()); err != nil {
|
||||
if err := function(getResponse()); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
}
|
||||
@@ -95,19 +107,17 @@ func (gui *Gui) wrappedPromptConfirmationFunction(handlersManageFocus bool, func
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) deleteConfirmationView() {
|
||||
func (gui *Gui) clearConfirmationViewKeyBindings() {
|
||||
keybindingConfig := gui.Config.GetUserConfig().Keybinding
|
||||
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.Confirm), gocui.ModNone)
|
||||
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone)
|
||||
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.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
|
||||
// we've already closed it so we can just return
|
||||
if !gui.Views.Confirmation.Visible {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !handlersManageFocus {
|
||||
@@ -116,7 +126,10 @@ func (gui *Gui) closeConfirmationPrompt(handlersManageFocus bool) error {
|
||||
}
|
||||
}
|
||||
|
||||
gui.deleteConfirmationView()
|
||||
gui.clearConfirmationViewKeyBindings()
|
||||
gui.Views.Confirmation.Visible = false
|
||||
gui.Views.Suggestions.Visible = false
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -156,50 +169,63 @@ func (gui *Gui) getConfirmationPanelDimensions(wrap bool, prompt string) (int, i
|
||||
height/2 + panelHeight/2
|
||||
}
|
||||
|
||||
func (gui *Gui) prepareConfirmationPanel(title, prompt string, hasLoader bool) (*gocui.View, error) {
|
||||
func (gui *Gui) prepareConfirmationPanel(title, prompt string, hasLoader bool, findSuggestionsFunc func(string) []*types.Suggestion) error {
|
||||
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(true, prompt)
|
||||
confirmationView, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0)
|
||||
// calling SetView on an existing view returns the same view, so I'm not bothering
|
||||
// to reassign to gui.Views.Confirmation
|
||||
_, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0)
|
||||
if err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return nil, err
|
||||
}
|
||||
confirmationView.HasLoader = hasLoader
|
||||
if hasLoader {
|
||||
gui.g.StartTicking()
|
||||
}
|
||||
confirmationView.Title = title
|
||||
confirmationView.Wrap = true
|
||||
confirmationView.FgColor = theme.GocuiDefaultTextColor
|
||||
return err
|
||||
}
|
||||
gui.Views.Confirmation.HasLoader = hasLoader
|
||||
if hasLoader {
|
||||
gui.g.StartTicking()
|
||||
}
|
||||
gui.Views.Confirmation.Title = title
|
||||
gui.Views.Confirmation.Wrap = true
|
||||
gui.Views.Confirmation.FgColor = theme.GocuiDefaultTextColor
|
||||
|
||||
gui.findSuggestions = findSuggestionsFunc
|
||||
if findSuggestionsFunc != nil {
|
||||
suggestionsViewHeight := 11
|
||||
suggestionsView, err := gui.g.SetView("suggestions", x0, y1, x1, y1+suggestionsViewHeight, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
suggestionsView.Wrap = true
|
||||
suggestionsView.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.setSuggestions([]*types.Suggestion{})
|
||||
suggestionsView.Visible = true
|
||||
}
|
||||
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
return gui.switchContext(gui.Contexts.Confirmation.Context)
|
||||
return gui.pushContext(gui.State.Contexts.Confirmation)
|
||||
})
|
||||
return confirmationView, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
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 {
|
||||
gui.deleteConfirmationView()
|
||||
}
|
||||
confirmationView, err := gui.prepareConfirmationPanel(opts.title, opts.prompt, opts.hasLoader)
|
||||
// remove any previous keybindings
|
||||
gui.clearConfirmationViewKeyBindings()
|
||||
|
||||
err := gui.prepareConfirmationPanel(opts.title, opts.prompt, opts.hasLoader, opts.findSuggestionsFunc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
confirmationView.Editable = opts.editable
|
||||
gui.Views.Confirmation.Editable = opts.editable
|
||||
gui.Views.Confirmation.Editor = gocui.EditorFunc(gui.defaultEditor)
|
||||
|
||||
if opts.editable {
|
||||
go utils.Safe(func() {
|
||||
// TODO: remove this wait (right now if you remove it the EditGotoToEndOfLine method doesn't seem to work)
|
||||
time.Sleep(time.Millisecond)
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
confirmationView.EditGotoToEndOfLine()
|
||||
return nil
|
||||
})
|
||||
})
|
||||
if err := gui.Views.Confirmation.SetEditorContent(opts.prompt); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := gui.renderStringSync(gui.Views.Confirmation, opts.prompt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
gui.renderString("confirmation", opts.prompt)
|
||||
return gui.setKeyBindings(opts)
|
||||
})
|
||||
return nil
|
||||
@@ -214,23 +240,79 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
|
||||
},
|
||||
)
|
||||
|
||||
gui.renderString("options", actions)
|
||||
var onConfirm func(*gocui.Gui, *gocui.View) error
|
||||
gui.renderString(gui.Views.Options, actions)
|
||||
var onConfirm func() error
|
||||
if opts.handleConfirmPrompt != nil {
|
||||
onConfirm = gui.wrappedPromptConfirmationFunction(opts.handlersManageFocus, opts.handleConfirmPrompt)
|
||||
onConfirm = gui.wrappedPromptConfirmationFunction(opts.handlersManageFocus, opts.handleConfirmPrompt, func() string { return gui.Views.Confirmation.Buffer() })
|
||||
} else {
|
||||
onConfirm = gui.wrappedConfirmationFunction(opts.handlersManageFocus, opts.handleConfirm)
|
||||
}
|
||||
|
||||
keybindingConfig := gui.Config.GetUserConfig().Keybinding
|
||||
if err := gui.g.SetKeybinding("confirmation", nil, gui.getKey(keybindingConfig.Universal.Confirm), gocui.ModNone, onConfirm); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.g.SetKeybinding("confirmation", nil, gui.getKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone, onConfirm); err != nil {
|
||||
return err
|
||||
type confirmationKeybinding struct {
|
||||
viewName string
|
||||
key interface{}
|
||||
handler func() error
|
||||
}
|
||||
|
||||
return gui.g.SetKeybinding("confirmation", nil, gui.getKey(keybindingConfig.Universal.Return), gocui.ModNone, gui.wrappedConfirmationFunction(opts.handlersManageFocus, opts.handleClose))
|
||||
keybindingConfig := gui.Config.GetUserConfig().Keybinding
|
||||
onSuggestionConfirm := gui.wrappedPromptConfirmationFunction(opts.handlersManageFocus, opts.handleConfirmPrompt, func() string { return gui.getSelectedSuggestionValue() })
|
||||
|
||||
confirmationKeybindings := []confirmationKeybinding{
|
||||
{
|
||||
viewName: "confirmation",
|
||||
key: gui.getKey(keybindingConfig.Universal.Confirm),
|
||||
handler: onConfirm,
|
||||
},
|
||||
{
|
||||
viewName: "confirmation",
|
||||
key: gui.getKey(keybindingConfig.Universal.ConfirmAlt1),
|
||||
handler: onConfirm,
|
||||
},
|
||||
{
|
||||
viewName: "confirmation",
|
||||
key: gui.getKey(keybindingConfig.Universal.Return),
|
||||
handler: gui.wrappedConfirmationFunction(opts.handlersManageFocus, opts.handleClose),
|
||||
},
|
||||
{
|
||||
viewName: "confirmation",
|
||||
key: gui.getKey(keybindingConfig.Universal.TogglePanel),
|
||||
handler: func() error { return gui.replaceContext(gui.State.Contexts.Suggestions) },
|
||||
},
|
||||
{
|
||||
viewName: "suggestions",
|
||||
key: gui.getKey(keybindingConfig.Universal.Confirm),
|
||||
handler: onSuggestionConfirm,
|
||||
},
|
||||
{
|
||||
viewName: "suggestions",
|
||||
key: gui.getKey(keybindingConfig.Universal.ConfirmAlt1),
|
||||
handler: onSuggestionConfirm,
|
||||
},
|
||||
{
|
||||
viewName: "suggestions",
|
||||
key: gui.getKey(keybindingConfig.Universal.Return),
|
||||
handler: gui.wrappedConfirmationFunction(opts.handlersManageFocus, opts.handleClose),
|
||||
},
|
||||
{
|
||||
viewName: "suggestions",
|
||||
key: gui.getKey(keybindingConfig.Universal.TogglePanel),
|
||||
handler: func() error { return gui.replaceContext(gui.State.Contexts.Confirmation) },
|
||||
},
|
||||
}
|
||||
|
||||
for _, binding := range confirmationKeybindings {
|
||||
if err := gui.g.SetKeybinding(binding.viewName, nil, binding.key, gocui.ModNone, gui.wrappedHandler(binding.handler)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) wrappedHandler(f func() error) func(g *gocui.Gui, v *gocui.View) error {
|
||||
return func(g *gocui.Gui, v *gocui.View) error {
|
||||
return f()
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) createErrorPanel(message string) error {
|
||||
@@ -251,11 +333,5 @@ func (gui *Gui) surfaceError(err error) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, sentinelError := range gui.sentinelErrorsArr() {
|
||||
if err == sentinelError {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
|
||||
@@ -6,127 +6,25 @@ import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
type ContextKind int
|
||||
|
||||
const (
|
||||
SIDE_CONTEXT int = iota
|
||||
SIDE_CONTEXT ContextKind = iota
|
||||
MAIN_CONTEXT
|
||||
TEMPORARY_POPUP
|
||||
PERSISTENT_POPUP
|
||||
EXTRAS_CONTEXT
|
||||
)
|
||||
|
||||
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 = "search"
|
||||
COMMIT_MESSAGE_CONTEXT_KEY = "commitMessage"
|
||||
SUBMODULES_CONTEXT_KEY = "submodules"
|
||||
)
|
||||
|
||||
var allContextKeys = []string{
|
||||
STATUS_CONTEXT_KEY,
|
||||
FILES_CONTEXT_KEY,
|
||||
LOCAL_BRANCHES_CONTEXT_KEY,
|
||||
REMOTES_CONTEXT_KEY,
|
||||
REMOTE_BRANCHES_CONTEXT_KEY,
|
||||
TAGS_CONTEXT_KEY,
|
||||
BRANCH_COMMITS_CONTEXT_KEY,
|
||||
REFLOG_COMMITS_CONTEXT_KEY,
|
||||
SUB_COMMITS_CONTEXT_KEY,
|
||||
COMMIT_FILES_CONTEXT_KEY,
|
||||
STASH_CONTEXT_KEY,
|
||||
MAIN_NORMAL_CONTEXT_KEY,
|
||||
MAIN_MERGING_CONTEXT_KEY,
|
||||
MAIN_PATCH_BUILDING_CONTEXT_KEY,
|
||||
MAIN_STAGING_CONTEXT_KEY,
|
||||
MENU_CONTEXT_KEY,
|
||||
CREDENTIALS_CONTEXT_KEY,
|
||||
CONFIRMATION_CONTEXT_KEY,
|
||||
SEARCH_CONTEXT_KEY,
|
||||
COMMIT_MESSAGE_CONTEXT_KEY,
|
||||
SUBMODULES_CONTEXT_KEY,
|
||||
}
|
||||
|
||||
type SimpleContextNode struct {
|
||||
Context Context
|
||||
}
|
||||
|
||||
type RemotesContextNode struct {
|
||||
Context Context
|
||||
Branches SimpleContextNode
|
||||
}
|
||||
|
||||
type ContextTree struct {
|
||||
Status SimpleContextNode
|
||||
Files SimpleContextNode
|
||||
Submodules 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.Submodules.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,
|
||||
}
|
||||
}
|
||||
|
||||
type Context interface {
|
||||
HandleFocus() error
|
||||
HandleFocusLost() error
|
||||
HandleRender() error
|
||||
GetKind() int
|
||||
GetKind() ContextKind
|
||||
GetViewName() string
|
||||
GetWindowName() string
|
||||
SetWindowName(string)
|
||||
GetKey() string
|
||||
GetKey() ContextKey
|
||||
SetParentContext(Context)
|
||||
|
||||
// we return a bool here to tell us whether or not the returned value just wraps a nil
|
||||
@@ -134,261 +32,22 @@ type Context interface {
|
||||
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()
|
||||
func (gui *Gui) popupViewNames() []string {
|
||||
result := []string{}
|
||||
for _, context := range gui.allContexts() {
|
||||
if context.GetKind() == PERSISTENT_POPUP || context.GetKind() == TEMPORARY_POPUP {
|
||||
result = append(result, context.GetViewName())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (c BasicContext) SetWindowName(windowName string) {
|
||||
panic("can't set window name on basic context")
|
||||
}
|
||||
func (gui *Gui) currentContextKeyIgnoringPopups() ContextKey {
|
||||
gui.State.ContextManager.RLock()
|
||||
defer gui.State.ContextManager.RUnlock()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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(),
|
||||
},
|
||||
Submodules: SimpleContextNode{
|
||||
Context: gui.submodulesListContext(),
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
"files": {
|
||||
{
|
||||
tab: "Files",
|
||||
contexts: []Context{gui.Contexts.Files.Context},
|
||||
},
|
||||
{
|
||||
tab: "Submodules",
|
||||
contexts: []Context{
|
||||
gui.Contexts.Submodules.Context,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) currentContextKeyIgnoringPopups() string {
|
||||
stack := gui.State.ContextStack
|
||||
stack := gui.State.ContextManager.ContextStack
|
||||
|
||||
for i := range stack {
|
||||
reversedIndex := len(stack) - 1 - i
|
||||
@@ -402,22 +61,18 @@ func (gui *Gui) currentContextKeyIgnoringPopups() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (gui *Gui) switchContext(c Context) error {
|
||||
// use replaceContext when you don't want to return to the original context upon
|
||||
// hitting escape: you want to go that context's parent instead.
|
||||
func (gui *Gui) replaceContext(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}
|
||||
gui.State.ContextManager.Lock()
|
||||
defer gui.State.ContextManager.Unlock()
|
||||
|
||||
if len(gui.State.ContextManager.ContextStack) == 0 {
|
||||
gui.State.ContextManager.ContextStack = []Context{c}
|
||||
} else {
|
||||
// TODO: think about other exceptional cases
|
||||
gui.State.ContextStack = append(gui.State.ContextStack, c)
|
||||
// replace the last item with the given item
|
||||
gui.State.ContextManager.ContextStack = append(gui.State.ContextManager.ContextStack[0:len(gui.State.ContextManager.ContextStack)-1], c)
|
||||
}
|
||||
|
||||
return gui.activateContext(c)
|
||||
@@ -426,28 +81,69 @@ func (gui *Gui) switchContext(c Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// switchContextToView is to be used when you don't know which context you
|
||||
func (gui *Gui) pushContext(c Context) error {
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
return gui.pushContextDirect(c)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) pushContextDirect(c Context) error {
|
||||
gui.State.ContextManager.Lock()
|
||||
|
||||
// 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.ContextManager.ContextStack {
|
||||
if stackContext.GetKey() != c.GetKey() {
|
||||
if err := gui.deactivateContext(stackContext); err != nil {
|
||||
gui.State.ContextManager.Unlock()
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
gui.State.ContextManager.ContextStack = []Context{c}
|
||||
} else if len(gui.State.ContextManager.ContextStack) == 0 || gui.currentContextWithoutLock().GetKey() != c.GetKey() {
|
||||
// Do not append if the one at the end is the same context (e.g. opening a menu from a menu)
|
||||
// In that case we'll just close the menu entirely when the user hits escape.
|
||||
|
||||
// TODO: think about other exceptional cases
|
||||
gui.State.ContextManager.ContextStack = append(gui.State.ContextManager.ContextStack, c)
|
||||
}
|
||||
|
||||
gui.State.ContextManager.Unlock()
|
||||
|
||||
return gui.activateContext(c)
|
||||
}
|
||||
|
||||
// asynchronous code idea: functions return an error via a channel, when done
|
||||
|
||||
// pushContextWithView 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) pushContextWithView(viewName string) error {
|
||||
return gui.pushContext(gui.State.ViewContextMap[viewName])
|
||||
}
|
||||
|
||||
func (gui *Gui) returnFromContext() error {
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
// TODO: add mutexes
|
||||
gui.State.ContextManager.Lock()
|
||||
|
||||
if len(gui.State.ContextStack) == 1 {
|
||||
if len(gui.State.ContextManager.ContextStack) == 1 {
|
||||
// cannot escape from bottommost context
|
||||
gui.State.ContextManager.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
n := len(gui.State.ContextStack) - 1
|
||||
n := len(gui.State.ContextManager.ContextStack) - 1
|
||||
|
||||
currentContext := gui.State.ContextStack[n]
|
||||
newContext := gui.State.ContextStack[n-1]
|
||||
currentContext := gui.State.ContextManager.ContextStack[n]
|
||||
newContext := gui.State.ContextManager.ContextStack[n-1]
|
||||
|
||||
gui.State.ContextStack = gui.State.ContextStack[:n]
|
||||
gui.State.ContextManager.ContextStack = gui.State.ContextManager.ContextStack[:n]
|
||||
|
||||
gui.State.ContextManager.Unlock()
|
||||
|
||||
if err := gui.deactivateContext(currentContext); err != nil {
|
||||
return err
|
||||
@@ -460,9 +156,17 @@ func (gui *Gui) returnFromContext() error {
|
||||
}
|
||||
|
||||
func (gui *Gui) deactivateContext(c Context) error {
|
||||
view, _ := gui.g.View(c.GetViewName())
|
||||
|
||||
if view != nil && view.IsSearching() {
|
||||
if err := gui.onSearchEscape(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 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 view != nil && c.GetKind() == TEMPORARY_POPUP || c.GetKind() == PERSISTENT_POPUP || c.GetKey() == COMMIT_FILES_CONTEXT_KEY {
|
||||
view.Visible = false
|
||||
}
|
||||
|
||||
if err := c.HandleFocusLost(); err != nil {
|
||||
@@ -481,7 +185,7 @@ func (gui *Gui) postRefreshUpdate(c Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if v.Context != c.GetKey() {
|
||||
if ContextKey(v.Context) != c.GetKey() {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -501,32 +205,27 @@ func (gui *Gui) postRefreshUpdate(c Context) error {
|
||||
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()
|
||||
return err
|
||||
}
|
||||
originalViewContextKey := v.Context
|
||||
originalViewContextKey := ContextKey(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)
|
||||
gui.setViewAsActiveForWindow(v)
|
||||
|
||||
if viewName == "main" {
|
||||
gui.changeMainViewsContext(c.GetKey())
|
||||
} else {
|
||||
gui.changeMainViewsContext("normal")
|
||||
gui.changeMainViewsContext(MAIN_NORMAL_CONTEXT_KEY)
|
||||
}
|
||||
|
||||
gui.setViewTabForContext(c)
|
||||
|
||||
if _, err := gui.g.SetCurrentView(viewName); err != nil {
|
||||
// if view no longer exists, pop again
|
||||
return gui.returnFromContext()
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := gui.g.SetViewOnTop(viewName); err != nil {
|
||||
// if view no longer exists, pop again
|
||||
return gui.returnFromContext()
|
||||
}
|
||||
v.Visible = true
|
||||
|
||||
// if the new context's view was previously displaying another context, render the new context
|
||||
if originalViewContextKey != c.GetKey() {
|
||||
@@ -535,7 +234,7 @@ func (gui *Gui) activateContext(c Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
v.Context = c.GetKey()
|
||||
v.Context = string(c.GetKey())
|
||||
|
||||
gui.g.Cursor = v.Editable
|
||||
|
||||
@@ -556,28 +255,50 @@ func (gui *Gui) activateContext(c Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) renderContextStack() string {
|
||||
result := ""
|
||||
for _, context := range gui.State.ContextStack {
|
||||
result += context.GetKey() + "\n"
|
||||
}
|
||||
return result
|
||||
}
|
||||
// // currently unused
|
||||
// func (gui *Gui) renderContextStack() string {
|
||||
// result := ""
|
||||
// for _, context := range gui.State.ContextManager.ContextStack {
|
||||
// result += string(context.GetKey()) + "\n"
|
||||
// }
|
||||
// return result
|
||||
// }
|
||||
|
||||
func (gui *Gui) currentContext() Context {
|
||||
if len(gui.State.ContextStack) == 0 {
|
||||
gui.State.ContextManager.RLock()
|
||||
defer gui.State.ContextManager.RUnlock()
|
||||
|
||||
return gui.currentContextWithoutLock()
|
||||
}
|
||||
|
||||
func (gui *Gui) currentContextWithoutLock() Context {
|
||||
if len(gui.State.ContextManager.ContextStack) == 0 {
|
||||
return gui.defaultSideContext()
|
||||
}
|
||||
|
||||
return gui.State.ContextStack[len(gui.State.ContextStack)-1]
|
||||
return gui.State.ContextManager.ContextStack[len(gui.State.ContextManager.ContextStack)-1]
|
||||
}
|
||||
|
||||
func (gui *Gui) currentSideContext() *ListContext {
|
||||
stack := gui.State.ContextStack
|
||||
// the status panel is not yet a list context (and may never be), so this method is not
|
||||
// quite the same as currentSideContext()
|
||||
func (gui *Gui) currentSideListContext() *ListContext {
|
||||
context := gui.currentSideContext()
|
||||
listContext, ok := context.(*ListContext)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return listContext
|
||||
}
|
||||
|
||||
func (gui *Gui) currentSideContext() Context {
|
||||
gui.State.ContextManager.RLock()
|
||||
defer gui.State.ContextManager.RUnlock()
|
||||
|
||||
stack := gui.State.ContextManager.ContextStack
|
||||
|
||||
// on startup the stack can be empty so we'll return an empty string in that case
|
||||
if len(stack) == 0 {
|
||||
return nil
|
||||
return gui.defaultSideContext()
|
||||
}
|
||||
|
||||
// find the first context in the stack with the type of SIDE_CONTEXT
|
||||
@@ -585,22 +306,45 @@ func (gui *Gui) currentSideContext() *ListContext {
|
||||
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 context
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return gui.defaultSideContext()
|
||||
}
|
||||
|
||||
// static as opposed to popup
|
||||
func (gui *Gui) currentStaticContext() Context {
|
||||
gui.State.ContextManager.RLock()
|
||||
defer gui.State.ContextManager.RUnlock()
|
||||
|
||||
stack := gui.State.ContextManager.ContextStack
|
||||
|
||||
if len(stack) == 0 {
|
||||
return gui.defaultSideContext()
|
||||
}
|
||||
|
||||
// find the first context in the stack without a popup type
|
||||
for i := range stack {
|
||||
context := stack[len(stack)-1-i]
|
||||
|
||||
if context.GetKind() != TEMPORARY_POPUP && context.GetKind() != PERSISTENT_POPUP {
|
||||
return context
|
||||
}
|
||||
}
|
||||
|
||||
return gui.defaultSideContext()
|
||||
}
|
||||
|
||||
func (gui *Gui) defaultSideContext() Context {
|
||||
return gui.Contexts.Files.Context
|
||||
if gui.State.Modes.Filtering.Active() {
|
||||
return gui.State.Contexts.BranchCommits
|
||||
} else {
|
||||
return gui.State.Contexts.Files
|
||||
}
|
||||
}
|
||||
|
||||
// remove the need to do this: always use a mapping
|
||||
func (gui *Gui) setInitialViewContexts() {
|
||||
// arguably we should only have our ViewContextMap and we should do away with
|
||||
// contexts on views, or vice versa
|
||||
@@ -611,7 +355,7 @@ func (gui *Gui) setInitialViewContexts() {
|
||||
continue
|
||||
}
|
||||
|
||||
view.Context = context.GetKey()
|
||||
view.Context = string(context.GetKey())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -641,18 +385,19 @@ func (gui *Gui) onViewFocusChange() error {
|
||||
|
||||
currentView := gui.g.CurrentView()
|
||||
for _, view := range gui.g.Views() {
|
||||
view.Highlight = view.Name() != "main" && view == currentView
|
||||
view.Highlight = view.Name() != "main" && view.Name() != "extras" && view == currentView
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) onViewFocusLost(v *gocui.View, newView *gocui.View) error {
|
||||
if v == nil {
|
||||
func (gui *Gui) onViewFocusLost(oldView *gocui.View, newView *gocui.View) error {
|
||||
if oldView == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if v.IsSearching() && newView.Name() != "search" {
|
||||
if err := gui.onSearchEscape(); err != nil {
|
||||
if oldView == gui.Views.CommitFiles && newView != gui.Views.Main && newView != gui.Views.Secondary && newView != gui.Views.Search {
|
||||
gui.resetWindowForView(gui.Views.CommitFiles)
|
||||
if err := gui.deactivateContext(gui.State.Contexts.CommitFiles); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -664,15 +409,15 @@ func (gui *Gui) onViewFocusLost(v *gocui.View, newView *gocui.View) error {
|
||||
// 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(contextKey string) {
|
||||
func (gui *Gui) changeMainViewsContext(contextKey ContextKey) {
|
||||
if gui.State.MainContext == contextKey {
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
gui.Views.Main.Context = string(contextKey)
|
||||
gui.Views.Secondary.Context = string(contextKey)
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown context for main: %s", contextKey))
|
||||
}
|
||||
@@ -681,7 +426,7 @@ func (gui *Gui) changeMainViewsContext(contextKey string) {
|
||||
}
|
||||
|
||||
func (gui *Gui) viewTabNames(viewName string) []string {
|
||||
tabContexts := gui.ViewTabContextMap[viewName]
|
||||
tabContexts := gui.State.ViewTabContextMap[viewName]
|
||||
|
||||
if len(tabContexts) == 0 {
|
||||
return nil
|
||||
@@ -697,7 +442,7 @@ func (gui *Gui) viewTabNames(viewName string) []string {
|
||||
|
||||
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()]
|
||||
tabContexts, ok := gui.State.ViewTabContextMap[c.GetViewName()]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -723,7 +468,7 @@ type tabContext struct {
|
||||
contexts []Context
|
||||
}
|
||||
|
||||
func (gui *Gui) mustContextForContextKey(contextKey string) Context {
|
||||
func (gui *Gui) mustContextForContextKey(contextKey ContextKey) Context {
|
||||
context, ok := gui.contextForContextKey(contextKey)
|
||||
|
||||
if !ok {
|
||||
@@ -733,7 +478,7 @@ func (gui *Gui) mustContextForContextKey(contextKey string) Context {
|
||||
return context
|
||||
}
|
||||
|
||||
func (gui *Gui) contextForContextKey(contextKey string) (Context, bool) {
|
||||
func (gui *Gui) contextForContextKey(contextKey ContextKey) (Context, bool) {
|
||||
for _, context := range gui.allContexts() {
|
||||
if context.GetKey() == contextKey {
|
||||
return context, true
|
||||
@@ -743,31 +488,15 @@ func (gui *Gui) contextForContextKey(contextKey string) (Context, bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (gui *Gui) rerenderView(viewName string) error {
|
||||
v, err := gui.g.View(viewName)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
contextKey := v.Context
|
||||
func (gui *Gui) rerenderView(view *gocui.View) error {
|
||||
contextKey := ContextKey(view.Context)
|
||||
context := gui.mustContextForContextKey(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()
|
||||
currentSideContext := gui.currentSideListContext()
|
||||
if currentSideContext == nil {
|
||||
return ""
|
||||
}
|
||||
@@ -780,3 +509,24 @@ func (gui *Gui) getSideContextSelectedItemId() string {
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// currently unused
|
||||
// func (gui *Gui) getCurrentSideView() *gocui.View {
|
||||
// currentSideContext := gui.currentSideContext()
|
||||
// if currentSideContext == nil {
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// view, _ := gui.g.View(currentSideContext.GetViewName())
|
||||
|
||||
// return view
|
||||
// }
|
||||
|
||||
// currently unused
|
||||
// func (gui *Gui) renderContextStack() string {
|
||||
// result := ""
|
||||
// for _, context := range gui.State.ContextManager.ContextStack {
|
||||
// result += context.GetViewName() + "\n"
|
||||
// }
|
||||
// return result
|
||||
// }
|
||||
|
||||
266
pkg/gui/context_config.go
Normal file
266
pkg/gui/context_config.go
Normal file
@@ -0,0 +1,266 @@
|
||||
package gui
|
||||
|
||||
type ContextKey string
|
||||
|
||||
const (
|
||||
STATUS_CONTEXT_KEY ContextKey = "status"
|
||||
FILES_CONTEXT_KEY ContextKey = "files"
|
||||
LOCAL_BRANCHES_CONTEXT_KEY ContextKey = "localBranches"
|
||||
REMOTES_CONTEXT_KEY ContextKey = "remotes"
|
||||
REMOTE_BRANCHES_CONTEXT_KEY ContextKey = "remoteBranches"
|
||||
TAGS_CONTEXT_KEY ContextKey = "tags"
|
||||
BRANCH_COMMITS_CONTEXT_KEY ContextKey = "commits"
|
||||
REFLOG_COMMITS_CONTEXT_KEY ContextKey = "reflogCommits"
|
||||
SUB_COMMITS_CONTEXT_KEY ContextKey = "subCommits"
|
||||
COMMIT_FILES_CONTEXT_KEY ContextKey = "commitFiles"
|
||||
STASH_CONTEXT_KEY ContextKey = "stash"
|
||||
MAIN_NORMAL_CONTEXT_KEY ContextKey = "normal"
|
||||
MAIN_MERGING_CONTEXT_KEY ContextKey = "merging"
|
||||
MAIN_PATCH_BUILDING_CONTEXT_KEY ContextKey = "patchBuilding"
|
||||
MAIN_STAGING_CONTEXT_KEY ContextKey = "staging"
|
||||
MENU_CONTEXT_KEY ContextKey = "menu"
|
||||
CREDENTIALS_CONTEXT_KEY ContextKey = "credentials"
|
||||
CONFIRMATION_CONTEXT_KEY ContextKey = "confirmation"
|
||||
SEARCH_CONTEXT_KEY ContextKey = "search"
|
||||
COMMIT_MESSAGE_CONTEXT_KEY ContextKey = "commitMessage"
|
||||
SUBMODULES_CONTEXT_KEY ContextKey = "submodules"
|
||||
SUGGESTIONS_CONTEXT_KEY ContextKey = "suggestions"
|
||||
COMMAND_LOG_CONTEXT_KEY ContextKey = "cmdLog"
|
||||
)
|
||||
|
||||
var allContextKeys = []ContextKey{
|
||||
STATUS_CONTEXT_KEY,
|
||||
FILES_CONTEXT_KEY,
|
||||
LOCAL_BRANCHES_CONTEXT_KEY,
|
||||
REMOTES_CONTEXT_KEY,
|
||||
REMOTE_BRANCHES_CONTEXT_KEY,
|
||||
TAGS_CONTEXT_KEY,
|
||||
BRANCH_COMMITS_CONTEXT_KEY,
|
||||
REFLOG_COMMITS_CONTEXT_KEY,
|
||||
SUB_COMMITS_CONTEXT_KEY,
|
||||
COMMIT_FILES_CONTEXT_KEY,
|
||||
STASH_CONTEXT_KEY,
|
||||
MAIN_NORMAL_CONTEXT_KEY,
|
||||
MAIN_MERGING_CONTEXT_KEY,
|
||||
MAIN_PATCH_BUILDING_CONTEXT_KEY,
|
||||
MAIN_STAGING_CONTEXT_KEY,
|
||||
MENU_CONTEXT_KEY,
|
||||
CREDENTIALS_CONTEXT_KEY,
|
||||
CONFIRMATION_CONTEXT_KEY,
|
||||
SEARCH_CONTEXT_KEY,
|
||||
COMMIT_MESSAGE_CONTEXT_KEY,
|
||||
SUBMODULES_CONTEXT_KEY,
|
||||
SUGGESTIONS_CONTEXT_KEY,
|
||||
COMMAND_LOG_CONTEXT_KEY,
|
||||
}
|
||||
|
||||
type ContextTree struct {
|
||||
Status Context
|
||||
Files *ListContext
|
||||
Submodules *ListContext
|
||||
Menu *ListContext
|
||||
Branches *ListContext
|
||||
Remotes *ListContext
|
||||
RemoteBranches *ListContext
|
||||
Tags *ListContext
|
||||
BranchCommits *ListContext
|
||||
CommitFiles *ListContext
|
||||
ReflogCommits *ListContext
|
||||
SubCommits *ListContext
|
||||
Stash *ListContext
|
||||
Suggestions *ListContext
|
||||
Normal Context
|
||||
Staging Context
|
||||
PatchBuilding Context
|
||||
Merging Context
|
||||
Credentials Context
|
||||
Confirmation Context
|
||||
CommitMessage Context
|
||||
Search Context
|
||||
CommandLog Context
|
||||
}
|
||||
|
||||
func (gui *Gui) allContexts() []Context {
|
||||
return []Context{
|
||||
gui.State.Contexts.Status,
|
||||
gui.State.Contexts.Files,
|
||||
gui.State.Contexts.Submodules,
|
||||
gui.State.Contexts.Branches,
|
||||
gui.State.Contexts.Remotes,
|
||||
gui.State.Contexts.RemoteBranches,
|
||||
gui.State.Contexts.Tags,
|
||||
gui.State.Contexts.BranchCommits,
|
||||
gui.State.Contexts.CommitFiles,
|
||||
gui.State.Contexts.ReflogCommits,
|
||||
gui.State.Contexts.Stash,
|
||||
gui.State.Contexts.Menu,
|
||||
gui.State.Contexts.Confirmation,
|
||||
gui.State.Contexts.Credentials,
|
||||
gui.State.Contexts.CommitMessage,
|
||||
gui.State.Contexts.Normal,
|
||||
gui.State.Contexts.Staging,
|
||||
gui.State.Contexts.Merging,
|
||||
gui.State.Contexts.PatchBuilding,
|
||||
gui.State.Contexts.SubCommits,
|
||||
gui.State.Contexts.Suggestions,
|
||||
gui.State.Contexts.CommandLog,
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) contextTree() ContextTree {
|
||||
return ContextTree{
|
||||
Status: &BasicContext{
|
||||
OnFocus: gui.handleStatusSelect,
|
||||
Kind: SIDE_CONTEXT,
|
||||
ViewName: "status",
|
||||
Key: STATUS_CONTEXT_KEY,
|
||||
},
|
||||
Files: gui.filesListContext(),
|
||||
Submodules: gui.submodulesListContext(),
|
||||
Menu: gui.menuListContext(),
|
||||
Remotes: gui.remotesListContext(),
|
||||
RemoteBranches: gui.remoteBranchesListContext(),
|
||||
BranchCommits: gui.branchCommitsListContext(),
|
||||
CommitFiles: gui.commitFilesListContext(),
|
||||
ReflogCommits: gui.reflogCommitsListContext(),
|
||||
SubCommits: gui.subCommitsListContext(),
|
||||
Branches: gui.branchesListContext(),
|
||||
Tags: gui.tagsListContext(),
|
||||
Stash: gui.stashListContext(),
|
||||
Normal: &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: &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: &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: &BasicContext{
|
||||
OnFocus: gui.refreshMergePanelWithLock,
|
||||
Kind: MAIN_CONTEXT,
|
||||
ViewName: "main",
|
||||
Key: MAIN_MERGING_CONTEXT_KEY,
|
||||
OnGetOptionsMap: gui.getMergingOptions,
|
||||
},
|
||||
Credentials: &BasicContext{
|
||||
OnFocus: gui.handleCredentialsViewFocused,
|
||||
Kind: PERSISTENT_POPUP,
|
||||
ViewName: "credentials",
|
||||
Key: CREDENTIALS_CONTEXT_KEY,
|
||||
},
|
||||
Confirmation: &BasicContext{
|
||||
OnFocus: func() error { return nil },
|
||||
Kind: TEMPORARY_POPUP,
|
||||
ViewName: "confirmation",
|
||||
Key: CONFIRMATION_CONTEXT_KEY,
|
||||
},
|
||||
Suggestions: gui.suggestionsListContext(),
|
||||
CommitMessage: &BasicContext{
|
||||
OnFocus: gui.handleCommitMessageFocused,
|
||||
Kind: PERSISTENT_POPUP,
|
||||
ViewName: "commitMessage",
|
||||
Key: COMMIT_MESSAGE_CONTEXT_KEY,
|
||||
},
|
||||
Search: &BasicContext{
|
||||
OnFocus: func() error { return nil },
|
||||
Kind: PERSISTENT_POPUP,
|
||||
ViewName: "search",
|
||||
Key: SEARCH_CONTEXT_KEY,
|
||||
},
|
||||
CommandLog: &BasicContext{
|
||||
OnFocus: func() error { return nil },
|
||||
Kind: EXTRAS_CONTEXT,
|
||||
ViewName: "extras",
|
||||
Key: COMMAND_LOG_CONTEXT_KEY,
|
||||
OnGetOptionsMap: gui.getMergingOptions,
|
||||
OnFocusLost: func() error {
|
||||
gui.Views.Extras.Autoscroll = true
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (tree ContextTree) initialViewContextMap() map[string]Context {
|
||||
return map[string]Context{
|
||||
"status": tree.Status,
|
||||
"files": tree.Files,
|
||||
"branches": tree.Branches,
|
||||
"commits": tree.BranchCommits,
|
||||
"commitFiles": tree.CommitFiles,
|
||||
"stash": tree.Stash,
|
||||
"menu": tree.Menu,
|
||||
"confirmation": tree.Confirmation,
|
||||
"credentials": tree.Credentials,
|
||||
"commitMessage": tree.CommitMessage,
|
||||
"main": tree.Normal,
|
||||
"secondary": tree.Normal,
|
||||
"extras": tree.CommandLog,
|
||||
}
|
||||
}
|
||||
|
||||
func (tree ContextTree) initialViewTabContextMap() map[string][]tabContext {
|
||||
return map[string][]tabContext{
|
||||
"branches": {
|
||||
{
|
||||
tab: "Local Branches",
|
||||
contexts: []Context{tree.Branches},
|
||||
},
|
||||
{
|
||||
tab: "Remotes",
|
||||
contexts: []Context{
|
||||
tree.Remotes,
|
||||
tree.RemoteBranches,
|
||||
},
|
||||
},
|
||||
{
|
||||
tab: "Tags",
|
||||
contexts: []Context{tree.Tags},
|
||||
},
|
||||
},
|
||||
"commits": {
|
||||
{
|
||||
tab: "Commits",
|
||||
contexts: []Context{tree.BranchCommits},
|
||||
},
|
||||
{
|
||||
tab: "Reflog",
|
||||
contexts: []Context{
|
||||
tree.ReflogCommits,
|
||||
},
|
||||
},
|
||||
},
|
||||
"files": {
|
||||
{
|
||||
tab: "Files",
|
||||
contexts: []Context{tree.Files},
|
||||
},
|
||||
{
|
||||
tab: "Submodules",
|
||||
contexts: []Context{
|
||||
tree.Submodules,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -13,19 +13,20 @@ type credentials chan string
|
||||
func (gui *Gui) promptUserForCredential(passOrUname string) string {
|
||||
gui.credentials = make(chan string)
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
credentialsView, _ := g.View("credentials")
|
||||
if passOrUname == "username" {
|
||||
credentialsView := gui.Views.Credentials
|
||||
switch passOrUname {
|
||||
case "username":
|
||||
credentialsView.Title = gui.Tr.CredentialsUsername
|
||||
credentialsView.Mask = 0
|
||||
} else if passOrUname == "password" {
|
||||
case "password":
|
||||
credentialsView.Title = gui.Tr.CredentialsPassword
|
||||
credentialsView.Mask = '*'
|
||||
} else {
|
||||
default:
|
||||
credentialsView.Title = gui.Tr.CredentialsPassphrase
|
||||
credentialsView.Mask = '*'
|
||||
}
|
||||
|
||||
if err := gui.switchContext(gui.Contexts.Credentials.Context); err != nil {
|
||||
if err := gui.pushContext(gui.State.Contexts.Credentials); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -38,18 +39,19 @@ func (gui *Gui) promptUserForCredential(passOrUname string) string {
|
||||
return userInput + "\n"
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSubmitCredential(g *gocui.Gui, v *gocui.View) error {
|
||||
message := gui.trimmedContent(v)
|
||||
func (gui *Gui) handleSubmitCredential() error {
|
||||
credentialsView := gui.Views.Credentials
|
||||
message := gui.trimmedContent(credentialsView)
|
||||
gui.credentials <- message
|
||||
gui.clearEditorView(v)
|
||||
gui.clearEditorView(credentialsView)
|
||||
if err := gui.returnFromContext(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{})
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCloseCredentialsView(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCloseCredentialsView() error {
|
||||
gui.credentials <- ""
|
||||
return gui.returnFromContext()
|
||||
}
|
||||
@@ -65,7 +67,7 @@ func (gui *Gui) handleCredentialsViewFocused() error {
|
||||
},
|
||||
)
|
||||
|
||||
gui.renderString("options", message)
|
||||
gui.renderString(gui.Views.Options, message)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -76,8 +78,9 @@ func (gui *Gui) handleCredentialsPopup(cmdErr error) {
|
||||
if strings.Contains(errMessage, "Invalid username, password or passphrase") {
|
||||
errMessage = gui.Tr.PassUnameWrong
|
||||
}
|
||||
_ = gui.returnFromContext()
|
||||
// we are not logging this error because it may contain a password or a passphrase
|
||||
gui.createErrorPanel(errMessage)
|
||||
_ = gui.createErrorPanel(errMessage)
|
||||
} else {
|
||||
_ = gui.closeConfirmationPrompt(false)
|
||||
}
|
||||
|
||||
@@ -12,34 +12,38 @@ import (
|
||||
)
|
||||
|
||||
type CustomCommandObjects struct {
|
||||
SelectedLocalCommit *models.Commit
|
||||
SelectedReflogCommit *models.Commit
|
||||
SelectedSubCommit *models.Commit
|
||||
SelectedFile *models.File
|
||||
SelectedLocalBranch *models.Branch
|
||||
SelectedRemoteBranch *models.RemoteBranch
|
||||
SelectedRemote *models.Remote
|
||||
SelectedTag *models.Tag
|
||||
SelectedStashEntry *models.StashEntry
|
||||
SelectedCommitFile *models.CommitFile
|
||||
CheckedOutBranch *models.Branch
|
||||
PromptResponses []string
|
||||
SelectedLocalCommit *models.Commit
|
||||
SelectedReflogCommit *models.Commit
|
||||
SelectedSubCommit *models.Commit
|
||||
SelectedFile *models.File
|
||||
SelectedPath string
|
||||
SelectedLocalBranch *models.Branch
|
||||
SelectedRemoteBranch *models.RemoteBranch
|
||||
SelectedRemote *models.Remote
|
||||
SelectedTag *models.Tag
|
||||
SelectedStashEntry *models.StashEntry
|
||||
SelectedCommitFile *models.CommitFile
|
||||
SelectedCommitFilePath string
|
||||
CheckedOutBranch *models.Branch
|
||||
PromptResponses []string
|
||||
}
|
||||
|
||||
func (gui *Gui) resolveTemplate(templateStr string, promptResponses []string) (string, error) {
|
||||
objects := CustomCommandObjects{
|
||||
SelectedFile: gui.getSelectedFile(),
|
||||
SelectedLocalCommit: gui.getSelectedLocalCommit(),
|
||||
SelectedReflogCommit: gui.getSelectedReflogCommit(),
|
||||
SelectedLocalBranch: gui.getSelectedBranch(),
|
||||
SelectedRemoteBranch: gui.getSelectedRemoteBranch(),
|
||||
SelectedRemote: gui.getSelectedRemote(),
|
||||
SelectedTag: gui.getSelectedTag(),
|
||||
SelectedStashEntry: gui.getSelectedStashEntry(),
|
||||
SelectedCommitFile: gui.getSelectedCommitFile(),
|
||||
SelectedSubCommit: gui.getSelectedSubCommit(),
|
||||
CheckedOutBranch: gui.currentBranch(),
|
||||
PromptResponses: promptResponses,
|
||||
SelectedFile: gui.getSelectedFile(),
|
||||
SelectedPath: gui.getSelectedPath(),
|
||||
SelectedLocalCommit: gui.getSelectedLocalCommit(),
|
||||
SelectedReflogCommit: gui.getSelectedReflogCommit(),
|
||||
SelectedLocalBranch: gui.getSelectedBranch(),
|
||||
SelectedRemoteBranch: gui.getSelectedRemoteBranch(),
|
||||
SelectedRemote: gui.getSelectedRemote(),
|
||||
SelectedTag: gui.getSelectedTag(),
|
||||
SelectedStashEntry: gui.getSelectedStashEntry(),
|
||||
SelectedCommitFile: gui.getSelectedCommitFile(),
|
||||
SelectedCommitFilePath: gui.getSelectedCommitFilePath(),
|
||||
SelectedSubCommit: gui.getSelectedSubCommit(),
|
||||
CheckedOutBranch: gui.currentBranch(),
|
||||
PromptResponses: promptResponses,
|
||||
}
|
||||
|
||||
return utils.ResolveTemplate(templateStr, objects)
|
||||
@@ -56,8 +60,7 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
|
||||
}
|
||||
|
||||
if customCommand.Subprocess {
|
||||
gui.PrepareSubProcess(cmdStr)
|
||||
return nil
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(gui.OSCommand.PrepareShellSubProcess(cmdStr))
|
||||
}
|
||||
|
||||
loadingText := customCommand.LoadingText
|
||||
@@ -65,9 +68,7 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
|
||||
loadingText = gui.Tr.LcRunningCustomCommandStatus
|
||||
}
|
||||
return gui.WithWaitingStatus(loadingText, func() error {
|
||||
gui.OSCommand.PrepareSubProcess(cmdStr)
|
||||
|
||||
if err := gui.OSCommand.RunCommand(cmdStr); err != nil {
|
||||
if err := gui.OSCommand.WithSpan(gui.Tr.Spans.CustomCommand).RunShellCommand(cmdStr); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{})
|
||||
@@ -98,15 +99,15 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.prompt(
|
||||
title,
|
||||
initialValue,
|
||||
func(str string) error {
|
||||
return gui.prompt(promptOpts{
|
||||
title: title,
|
||||
initialContent: initialValue,
|
||||
handleConfirm: func(str string) error {
|
||||
promptResponses[idx] = str
|
||||
|
||||
return wrappedF()
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
case "menu":
|
||||
f = func() error {
|
||||
@@ -175,9 +176,14 @@ func (gui *Gui) GetCustomCommandKeybindings() []*Binding {
|
||||
case "":
|
||||
log.Fatalf("Error parsing custom command keybindings: context not provided (use context: 'global' for the global context). Key: %s, Command: %s", customCommand.Key, customCommand.Command)
|
||||
default:
|
||||
context, ok := gui.contextForContextKey(customCommand.Context)
|
||||
context, ok := gui.contextForContextKey(ContextKey(customCommand.Context))
|
||||
// stupid golang making me build an array of strings for this.
|
||||
allContextKeyStrings := make([]string, len(allContextKeys))
|
||||
for i := range allContextKeys {
|
||||
allContextKeyStrings[i] = string(allContextKeys[i])
|
||||
}
|
||||
if !ok {
|
||||
log.Fatalf("Error when setting custom command keybindings: unknown context: %s. Key: %s, Command: %s.\nPermitted contexts: %s", customCommand.Context, customCommand.Key, customCommand.Command, strings.Join(allContextKeys, ", "))
|
||||
log.Fatalf("Error when setting custom command keybindings: unknown context: %s. Key: %s, Command: %s.\nPermitted contexts: %s", customCommand.Context, customCommand.Key, customCommand.Command, strings.Join(allContextKeyStrings, ", "))
|
||||
}
|
||||
// here we assume that a given context will always belong to the same view.
|
||||
// Currently this is a safe bet but it's by no means guaranteed in the long term
|
||||
@@ -196,7 +202,7 @@ func (gui *Gui) GetCustomCommandKeybindings() []*Binding {
|
||||
Contexts: contexts,
|
||||
Key: gui.getKey(customCommand.Key),
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.wrappedHandler(gui.handleCustomCommandKeybinding(customCommand)),
|
||||
Handler: gui.handleCustomCommandKeybinding(customCommand),
|
||||
Description: description,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ package gui
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
func (gui *Gui) exitDiffMode() error {
|
||||
@@ -16,7 +14,7 @@ func (gui *Gui) renderDiff() error {
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
fmt.Sprintf("git diff --submodule --no-ext-diff --color %s", gui.diffStr()),
|
||||
)
|
||||
task := gui.createRunPtyTask(cmd)
|
||||
task := NewRunPtyTask(cmd)
|
||||
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
main: &viewUpdateOpts{
|
||||
@@ -51,7 +49,7 @@ func (gui *Gui) currentDiffTerminals() []string {
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
context := gui.currentSideContext()
|
||||
context := gui.currentSideListContext()
|
||||
if context == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -96,17 +94,13 @@ func (gui *Gui) diffStr() string {
|
||||
if file != "" {
|
||||
output += " -- " + file
|
||||
} else if gui.State.Modes.Filtering.Active() {
|
||||
output += " -- " + gui.State.Modes.Filtering.Path
|
||||
output += " -- " + gui.State.Modes.Filtering.GetPath()
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateDiffingMenuPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateDiffingMenuPanel() error {
|
||||
names := gui.currentDiffTerminals()
|
||||
|
||||
menuItems := []*menuItem{}
|
||||
@@ -128,9 +122,12 @@ func (gui *Gui) handleCreateDiffingMenuPanel(g *gocui.Gui, v *gocui.View) error
|
||||
{
|
||||
displayString: gui.Tr.LcEnterRefToDiff,
|
||||
onPress: func() error {
|
||||
return gui.prompt(gui.Tr.LcEnteRefName, "", func(response string) error {
|
||||
gui.State.Modes.Diffing.Ref = strings.TrimSpace(response)
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.LcEnteRefName,
|
||||
handleConfirm: func(response string) error {
|
||||
gui.State.Modes.Diffing.Ref = strings.TrimSpace(response)
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,67 +1,79 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
)
|
||||
|
||||
func (gui *Gui) submoduleFromFile(file *models.File) *models.SubmoduleConfig {
|
||||
for _, config := range gui.State.Submodules {
|
||||
if config.Name == file.Name {
|
||||
return config
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateDiscardMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
func (gui *Gui) handleCreateDiscardMenu() error {
|
||||
node := gui.getSelectedFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var menuItems []*menuItem
|
||||
|
||||
submodules := gui.State.Submodules
|
||||
if file.IsSubmodule(submodules) {
|
||||
submodule := file.SubmoduleConfig(submodules)
|
||||
|
||||
menuItems = []*menuItem{
|
||||
{
|
||||
displayString: gui.Tr.LcSubmoduleStashAndReset,
|
||||
onPress: func() error {
|
||||
return gui.resetSubmodule(submodule)
|
||||
},
|
||||
},
|
||||
}
|
||||
} else {
|
||||
if node.File == nil {
|
||||
menuItems = []*menuItem{
|
||||
{
|
||||
displayString: gui.Tr.LcDiscardAllChanges,
|
||||
onPress: func() error {
|
||||
if err := gui.GitCommand.DiscardAllFileChanges(file); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.DiscardAllChangesInDirectory).DiscardAllDirChanges(node); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if file.HasStagedChanges && file.HasUnstagedChanges {
|
||||
if node.GetHasStagedChanges() && node.GetHasUnstagedChanges() {
|
||||
menuItems = append(menuItems, &menuItem{
|
||||
displayString: gui.Tr.LcDiscardUnstagedChanges,
|
||||
onPress: func() error {
|
||||
if err := gui.GitCommand.DiscardUnstagedFileChanges(file); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.DiscardUnstagedChangesInDirectory).DiscardUnstagedDirChanges(node); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}})
|
||||
},
|
||||
})
|
||||
}
|
||||
} else {
|
||||
file := node.File
|
||||
|
||||
submodules := gui.State.Submodules
|
||||
if file.IsSubmodule(submodules) {
|
||||
submodule := file.SubmoduleConfig(submodules)
|
||||
|
||||
menuItems = []*menuItem{
|
||||
{
|
||||
displayString: gui.Tr.LcSubmoduleStashAndReset,
|
||||
onPress: func() error {
|
||||
return gui.handleResetSubmodule(submodule)
|
||||
},
|
||||
},
|
||||
}
|
||||
} else {
|
||||
menuItems = []*menuItem{
|
||||
{
|
||||
displayString: gui.Tr.LcDiscardAllChanges,
|
||||
onPress: func() error {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.DiscardAllChangesInFile).DiscardAllFileChanges(file); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if file.HasStagedChanges && file.HasUnstagedChanges {
|
||||
menuItems = append(menuItems, &menuItem{
|
||||
displayString: gui.Tr.LcDiscardUnstagedChanges,
|
||||
onPress: func() error {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.DiscardAllUnstagedChangesInFile).DiscardUnstagedFileChanges(file); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}})
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return gui.createMenu(file.Name, menuItems, createMenuOptions{showCancel: true})
|
||||
return gui.createMenu(node.GetPath(), menuItems, createMenuOptions{showCancel: true})
|
||||
}
|
||||
|
||||
96
pkg/gui/editors.go
Normal file
96
pkg/gui/editors.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"unicode"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
// we've just copy+pasted the editor from gocui to here so that we can also re-
|
||||
// render the commit message length on each keypress
|
||||
func (gui *Gui) commitMessageEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
|
||||
newlineKey, ok := gui.getKey(gui.Config.GetUserConfig().Keybinding.Universal.AppendNewline).(gocui.Key)
|
||||
if !ok {
|
||||
newlineKey = gocui.KeyAltEnter
|
||||
}
|
||||
|
||||
matched := true
|
||||
switch {
|
||||
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
|
||||
v.EditDelete(true)
|
||||
case key == gocui.KeyCtrlD || key == gocui.KeyDelete:
|
||||
v.EditDelete(false)
|
||||
case key == gocui.KeyArrowDown:
|
||||
v.MoveCursor(0, 1, false)
|
||||
case key == gocui.KeyArrowUp:
|
||||
v.MoveCursor(0, -1, false)
|
||||
case key == gocui.KeyArrowLeft:
|
||||
v.MoveCursor(-1, 0, false)
|
||||
case key == gocui.KeyArrowRight:
|
||||
v.MoveCursor(1, 0, false)
|
||||
case key == newlineKey:
|
||||
v.EditNewLine()
|
||||
case key == gocui.KeySpace:
|
||||
v.EditWrite(' ')
|
||||
case key == gocui.KeyInsert:
|
||||
v.Overwrite = !v.Overwrite
|
||||
case key == gocui.KeyCtrlU:
|
||||
v.EditDeleteToStartOfLine()
|
||||
case key == gocui.KeyCtrlA:
|
||||
v.EditGotoToStartOfLine()
|
||||
case key == gocui.KeyCtrlE:
|
||||
v.EditGotoToEndOfLine()
|
||||
|
||||
// TODO: see if we need all three of these conditions: maybe the final one is sufficient
|
||||
case ch != 0 && mod == 0 && unicode.IsPrint(ch):
|
||||
v.EditWrite(ch)
|
||||
default:
|
||||
matched = false
|
||||
}
|
||||
|
||||
gui.RenderCommitLength()
|
||||
|
||||
return matched
|
||||
}
|
||||
|
||||
func (gui *Gui) defaultEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
|
||||
matched := true
|
||||
switch {
|
||||
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
|
||||
v.EditDelete(true)
|
||||
case key == gocui.KeyCtrlD || key == gocui.KeyDelete:
|
||||
v.EditDelete(false)
|
||||
case key == gocui.KeyArrowDown:
|
||||
v.MoveCursor(0, 1, false)
|
||||
case key == gocui.KeyArrowUp:
|
||||
v.MoveCursor(0, -1, false)
|
||||
case key == gocui.KeyArrowLeft:
|
||||
v.MoveCursor(-1, 0, false)
|
||||
case key == gocui.KeyArrowRight:
|
||||
v.MoveCursor(1, 0, false)
|
||||
case key == gocui.KeySpace:
|
||||
v.EditWrite(' ')
|
||||
case key == gocui.KeyInsert:
|
||||
v.Overwrite = !v.Overwrite
|
||||
case key == gocui.KeyCtrlU:
|
||||
v.EditDeleteToStartOfLine()
|
||||
case key == gocui.KeyCtrlA:
|
||||
v.EditGotoToStartOfLine()
|
||||
case key == gocui.KeyCtrlE:
|
||||
v.EditGotoToEndOfLine()
|
||||
|
||||
// TODO: see if we need all three of these conditions: maybe the final one is sufficient
|
||||
case ch != 0 && mod == 0 && unicode.IsPrint(ch):
|
||||
v.EditWrite(ch)
|
||||
default:
|
||||
matched = false
|
||||
}
|
||||
|
||||
if gui.findSuggestions != nil {
|
||||
input := v.Buffer()
|
||||
suggestions := gui.findSuggestions(input)
|
||||
gui.setSuggestions(suggestions)
|
||||
}
|
||||
|
||||
return matched
|
||||
}
|
||||
3
pkg/gui/errors.go
Normal file
3
pkg/gui/errors.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package gui
|
||||
|
||||
const UNKNOWN_VIEW_ERROR_MSG = "unknown view"
|
||||
49
pkg/gui/extras_panel.go
Normal file
49
pkg/gui/extras_panel.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package gui
|
||||
|
||||
func (gui *Gui) handleCreateExtrasMenuPanel() error {
|
||||
menuItems := []*menuItem{
|
||||
{
|
||||
displayString: gui.Tr.ToggleShowCommandLog,
|
||||
onPress: func() error {
|
||||
currentContext := gui.currentStaticContext()
|
||||
if gui.ShowExtrasWindow && currentContext.GetKey() == COMMAND_LOG_CONTEXT_KEY {
|
||||
if err := gui.returnFromContext(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
gui.ShowExtrasWindow = !gui.ShowExtrasWindow
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: gui.Tr.FocusCommandLog,
|
||||
onPress: func() error {
|
||||
return gui.handleFocusCommandLog()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return gui.createMenu(gui.Tr.CommandLog, menuItems, createMenuOptions{showCancel: true})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleFocusCommandLog() error {
|
||||
gui.ShowExtrasWindow = true
|
||||
gui.State.Contexts.CommandLog.SetParentContext(gui.currentSideContext())
|
||||
return gui.pushContext(gui.State.Contexts.CommandLog)
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollUpExtra() error {
|
||||
gui.Views.Extras.Autoscroll = false
|
||||
|
||||
return gui.scrollUpView(gui.Views.Extras)
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollDownExtra() error {
|
||||
gui.Views.Extras.Autoscroll = false
|
||||
|
||||
if err := gui.scrollDownView(gui.Views.Extras); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -28,21 +28,6 @@ func NewFileWatcher(log *logrus.Entry) *fileWatcher {
|
||||
return &fileWatcher{
|
||||
Disabled: true,
|
||||
}
|
||||
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return &fileWatcher{
|
||||
Disabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
return &fileWatcher{
|
||||
Watcher: watcher,
|
||||
Log: log,
|
||||
WatchedFilenames: make([]string, 0, MAX_WATCHED_FILES),
|
||||
}
|
||||
}
|
||||
|
||||
func (w *fileWatcher) watchingFilename(filename string) bool {
|
||||
@@ -132,7 +117,7 @@ func (gui *Gui) watchFilesForChanges() {
|
||||
}
|
||||
// only refresh if we're not already
|
||||
if !gui.State.IsRefreshingFiles {
|
||||
gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
|
||||
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}})
|
||||
}
|
||||
|
||||
// watch for errors
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
|
||||
// "io"
|
||||
// "io/ioutil"
|
||||
|
||||
// "strings"
|
||||
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -14,66 +8,88 @@ import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/mgutz/str"
|
||||
)
|
||||
|
||||
// list panel functions
|
||||
|
||||
func (gui *Gui) getSelectedFile() *models.File {
|
||||
func (gui *Gui) getSelectedFileNode() *filetree.FileNode {
|
||||
selectedLine := gui.State.Panels.Files.SelectedLineIdx
|
||||
if selectedLine == -1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.State.Files[selectedLine]
|
||||
return gui.State.FileManager.GetItemAtIndex(selectedLine)
|
||||
}
|
||||
|
||||
func (gui *Gui) getSelectedFile() *models.File {
|
||||
node := gui.getSelectedFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
return node.File
|
||||
}
|
||||
|
||||
func (gui *Gui) getSelectedPath() string {
|
||||
node := gui.getSelectedFileNode()
|
||||
if node == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return node.GetPath()
|
||||
}
|
||||
|
||||
func (gui *Gui) selectFile(alreadySelected bool) error {
|
||||
gui.getFilesView().FocusPoint(0, gui.State.Panels.Files.SelectedLineIdx)
|
||||
gui.Views.Files.FocusPoint(0, gui.State.Panels.Files.SelectedLineIdx)
|
||||
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
node := gui.getSelectedFileNode()
|
||||
|
||||
if node == nil {
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
main: &viewUpdateOpts{
|
||||
title: "",
|
||||
task: gui.createRenderStringTask(gui.Tr.NoChangedFiles),
|
||||
task: NewRenderStringTask(gui.Tr.NoChangedFiles),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if !alreadySelected {
|
||||
// TODO: pull into update task interface
|
||||
if err := gui.resetOrigin(gui.getMainView()); err != nil {
|
||||
if err := gui.resetOrigin(gui.Views.Main); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.resetOrigin(gui.getSecondaryView()); err != nil {
|
||||
if err := gui.resetOrigin(gui.Views.Secondary); err != nil {
|
||||
return err
|
||||
}
|
||||
gui.takeOverMergeConflictScrolling()
|
||||
}
|
||||
|
||||
if file.HasInlineMergeConflicts {
|
||||
return gui.refreshMergePanel()
|
||||
if node.File != nil && node.File.HasInlineMergeConflicts {
|
||||
return gui.refreshMergePanelWithLock()
|
||||
}
|
||||
|
||||
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(file, false, !file.HasUnstagedChanges && file.HasStagedChanges)
|
||||
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(node, false, !node.GetHasUnstagedChanges() && node.GetHasStagedChanges())
|
||||
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
|
||||
|
||||
refreshOpts := refreshMainOpts{main: &viewUpdateOpts{
|
||||
title: gui.Tr.UnstagedChanges,
|
||||
task: gui.createRunPtyTask(cmd),
|
||||
task: NewRunPtyTask(cmd),
|
||||
}}
|
||||
|
||||
if file.HasStagedChanges && file.HasUnstagedChanges {
|
||||
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(file, false, true)
|
||||
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
|
||||
if node.GetHasUnstagedChanges() {
|
||||
if node.GetHasStagedChanges() {
|
||||
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(node, false, true)
|
||||
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
|
||||
|
||||
refreshOpts.secondary = &viewUpdateOpts{
|
||||
title: gui.Tr.StagedChanges,
|
||||
task: gui.createRunPtyTask(cmd),
|
||||
refreshOpts.secondary = &viewUpdateOpts{
|
||||
title: gui.Tr.StagedChanges,
|
||||
task: NewRunPtyTask(cmd),
|
||||
}
|
||||
}
|
||||
} else if !file.HasUnstagedChanges {
|
||||
} else {
|
||||
refreshOpts.main.title = gui.Tr.StagedChanges
|
||||
}
|
||||
|
||||
@@ -88,13 +104,8 @@ func (gui *Gui) refreshFilesAndSubmodules() error {
|
||||
gui.Mutexes.RefreshingFilesMutex.Unlock()
|
||||
}()
|
||||
|
||||
selectedFile := gui.getSelectedFile()
|
||||
selectedPath := gui.getSelectedPath()
|
||||
|
||||
filesView := gui.getFilesView()
|
||||
if filesView == nil {
|
||||
// if the filesView hasn't been instantiated yet we just return
|
||||
return nil
|
||||
}
|
||||
if err := gui.refreshStateSubmoduleConfigs(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -103,20 +114,20 @@ func (gui *Gui) refreshFilesAndSubmodules() error {
|
||||
}
|
||||
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
if err := gui.postRefreshUpdate(gui.Contexts.Submodules.Context); err != nil {
|
||||
if err := gui.postRefreshUpdate(gui.State.Contexts.Submodules); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
|
||||
if gui.getFilesView().Context == FILES_CONTEXT_KEY {
|
||||
if ContextKey(gui.Views.Files.Context) == FILES_CONTEXT_KEY {
|
||||
// doing this a little custom (as opposed to using gui.postRefreshUpdate) because we handle selecting the file explicitly below
|
||||
if err := gui.Contexts.Files.Context.HandleRender(); err != nil {
|
||||
if err := gui.State.Contexts.Files.HandleRender(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if gui.currentContext().GetKey() == FILES_CONTEXT_KEY || (g.CurrentView() == gui.getMainView() && g.CurrentView().Context == MAIN_MERGING_CONTEXT_KEY) {
|
||||
newSelectedFile := gui.getSelectedFile()
|
||||
alreadySelected := selectedFile != nil && newSelectedFile != nil && newSelectedFile.Name == selectedFile.Name
|
||||
if gui.currentContext().GetKey() == FILES_CONTEXT_KEY || (g.CurrentView() == gui.Views.Main && ContextKey(g.CurrentView().Context) == MAIN_MERGING_CONTEXT_KEY) {
|
||||
newSelectedPath := gui.getSelectedPath()
|
||||
alreadySelected := selectedPath != "" && newSelectedPath == selectedPath
|
||||
if err := gui.selectFile(alreadySelected); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -131,7 +142,7 @@ func (gui *Gui) refreshFilesAndSubmodules() error {
|
||||
// specific functions
|
||||
|
||||
func (gui *Gui) stagedFiles() []*models.File {
|
||||
files := gui.State.Files
|
||||
files := gui.State.FileManager.GetAllFiles()
|
||||
result := make([]*models.File, 0)
|
||||
for _, file := range files {
|
||||
if file.HasStagedChanges {
|
||||
@@ -142,7 +153,7 @@ func (gui *Gui) stagedFiles() []*models.File {
|
||||
}
|
||||
|
||||
func (gui *Gui) trackedFiles() []*models.File {
|
||||
files := gui.State.Files
|
||||
files := gui.State.FileManager.GetAllFiles()
|
||||
result := make([]*models.File, 0, len(files))
|
||||
for _, file := range files {
|
||||
if file.Tracked {
|
||||
@@ -152,7 +163,7 @@ func (gui *Gui) trackedFiles() []*models.File {
|
||||
return result
|
||||
}
|
||||
|
||||
func (gui *Gui) stageSelectedFile(g *gocui.Gui) error {
|
||||
func (gui *Gui) stageSelectedFile() error {
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
return nil
|
||||
@@ -161,16 +172,22 @@ func (gui *Gui) stageSelectedFile(g *gocui.Gui) error {
|
||||
return gui.GitCommand.StageFile(file.Name)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleEnterFile(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleEnterFile() error {
|
||||
return gui.enterFile(false, -1)
|
||||
}
|
||||
|
||||
func (gui *Gui) enterFile(forceSecondaryFocused bool, selectedLineIdx int) error {
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
node := gui.getSelectedFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if node.File == nil {
|
||||
return gui.handleToggleDirCollapsed()
|
||||
}
|
||||
|
||||
file := node.File
|
||||
|
||||
submoduleConfigs := gui.State.Submodules
|
||||
if file.IsSubmodule(submoduleConfigs) {
|
||||
submoduleConfig := file.SubmoduleConfig(submoduleConfigs)
|
||||
@@ -183,32 +200,53 @@ func (gui *Gui) enterFile(forceSecondaryFocused bool, selectedLineIdx int) error
|
||||
if file.HasMergeConflicts {
|
||||
return gui.createErrorPanel(gui.Tr.FileStagingRequirements)
|
||||
}
|
||||
gui.switchContext(gui.Contexts.Staging.Context)
|
||||
_ = gui.pushContext(gui.State.Contexts.Staging)
|
||||
|
||||
return gui.handleRefreshStagingPanel(forceSecondaryFocused, selectedLineIdx) // TODO: check if this is broken, try moving into context code
|
||||
}
|
||||
|
||||
func (gui *Gui) handleFilePress() error {
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
node := gui.getSelectedFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if file.HasInlineMergeConflicts {
|
||||
return gui.handleSwitchToMerge()
|
||||
}
|
||||
if node.IsLeaf() {
|
||||
file := node.File
|
||||
|
||||
if file.HasUnstagedChanges {
|
||||
if err := gui.GitCommand.StageFile(file.Name); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
if file.HasInlineMergeConflicts {
|
||||
return gui.handleSwitchToMerge()
|
||||
}
|
||||
|
||||
if file.HasUnstagedChanges {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.StageFile).StageFile(file.Name); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
} else {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.UnstageFile).UnStageFile(file.Names(), file.Tracked); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := gui.GitCommand.UnStageFile(file.Name, file.Tracked); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
// if any files within have inline merge conflicts we can't stage or unstage,
|
||||
// or it'll end up with those >>>>>> lines actually staged
|
||||
if node.GetHasInlineMergeConflicts() {
|
||||
return gui.createErrorPanel(gui.Tr.ErrStageDirWithInlineMergeConflicts)
|
||||
}
|
||||
|
||||
if node.GetHasUnstagedChanges() {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.StageFile).StageFile(node.Path); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
} else {
|
||||
// pretty sure it doesn't matter that we're always passing true here
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.UnstageFile).UnStageFile([]string{node.Path}, true); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
|
||||
if err := gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -216,7 +254,7 @@ func (gui *Gui) handleFilePress() error {
|
||||
}
|
||||
|
||||
func (gui *Gui) allFilesStaged() bool {
|
||||
for _, file := range gui.State.Files {
|
||||
for _, file := range gui.State.FileManager.GetAllFiles() {
|
||||
if file.HasUnstagedChanges {
|
||||
return false
|
||||
}
|
||||
@@ -228,64 +266,88 @@ func (gui *Gui) focusAndSelectFile() error {
|
||||
return gui.selectFile(false)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStageAll(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleStageAll() error {
|
||||
var err error
|
||||
if gui.allFilesStaged() {
|
||||
err = gui.GitCommand.UnstageAll()
|
||||
err = gui.GitCommand.WithSpan(gui.Tr.Spans.UnstageAllFiles).UnstageAll()
|
||||
} else {
|
||||
err = gui.GitCommand.StageAll()
|
||||
err = gui.GitCommand.WithSpan(gui.Tr.Spans.StageAllFiles).StageAll()
|
||||
}
|
||||
if err != nil {
|
||||
_ = gui.surfaceError(err)
|
||||
}
|
||||
|
||||
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
|
||||
if err := gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.selectFile(false)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
func (gui *Gui) handleIgnoreFile() error {
|
||||
node := gui.getSelectedFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
if file.Name == ".gitignore" {
|
||||
|
||||
if node.GetPath() == ".gitignore" {
|
||||
return gui.createErrorPanel("Cannot ignore .gitignore")
|
||||
}
|
||||
|
||||
if file.Tracked {
|
||||
gitCommand := gui.GitCommand.WithSpan(gui.Tr.Spans.IgnoreFile)
|
||||
|
||||
unstageFiles := func() error {
|
||||
return node.ForEachFile(func(file *models.File) error {
|
||||
if file.HasStagedChanges {
|
||||
if err := gitCommand.UnStageFile(file.Names(), file.Tracked); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if node.GetIsTracked() {
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.IgnoreTracked,
|
||||
prompt: gui.Tr.IgnoreTrackedPrompt,
|
||||
handleConfirm: func() error {
|
||||
if err := gui.GitCommand.Ignore(file.Name); err != nil {
|
||||
// not 100% sure if this is necessary but I'll assume it is
|
||||
if err := unstageFiles(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.GitCommand.RemoveTrackedFiles(file.Name); err != nil {
|
||||
|
||||
if err := gitCommand.RemoveTrackedFiles(node.GetPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{FILES}})
|
||||
|
||||
if err := gitCommand.Ignore(node.GetPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if err := gui.GitCommand.Ignore(file.Name); err != nil {
|
||||
if err := unstageFiles(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gitCommand.Ignore(node.GetPath()); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{FILES}})
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleWIPCommitPress(g *gocui.Gui, filesView *gocui.View) error {
|
||||
skipHookPreifx := gui.Config.GetUserConfig().Git.SkipHookPrefix
|
||||
if skipHookPreifx == "" {
|
||||
func (gui *Gui) handleWIPCommitPress() error {
|
||||
skipHookPrefix := gui.Config.GetUserConfig().Git.SkipHookPrefix
|
||||
if skipHookPrefix == "" {
|
||||
return gui.createErrorPanel(gui.Tr.SkipHookPrefixNotConfigured)
|
||||
}
|
||||
|
||||
gui.renderStringSync("commitMessage", skipHookPreifx)
|
||||
if err := gui.getCommitMessageView().SetCursor(len(skipHookPreifx), 0); err != nil {
|
||||
if err := gui.Views.CommitMessage.SetEditorContent(skipHookPrefix); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -301,14 +363,33 @@ func (gui *Gui) commitPrefixConfigForRepo() *config.CommitPrefixConfig {
|
||||
return &cfg
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitPress() error {
|
||||
if len(gui.stagedFiles()) == 0 {
|
||||
return gui.promptToStageAllAndRetry(func() error {
|
||||
return gui.handleCommitPress()
|
||||
})
|
||||
func (gui *Gui) prepareFilesForCommit() error {
|
||||
noStagedFiles := len(gui.stagedFiles()) == 0
|
||||
if noStagedFiles && gui.Config.GetUserConfig().Gui.SkipNoStagedFilesWarning {
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.StageAllFiles).StageAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.refreshFilesAndSubmodules()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitPress() error {
|
||||
if err := gui.prepareFilesForCommit(); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
if gui.State.FileManager.GetItemsLength() == 0 {
|
||||
return gui.createErrorPanel(gui.Tr.NoFilesStagedTitle)
|
||||
}
|
||||
|
||||
if len(gui.stagedFiles()) == 0 {
|
||||
return gui.promptToStageAllAndRetry(gui.handleCommitPress)
|
||||
}
|
||||
|
||||
commitMessageView := gui.getCommitMessageView()
|
||||
commitPrefixConfig := gui.commitPrefixConfigForRepo()
|
||||
if commitPrefixConfig != nil {
|
||||
prefixPattern := commitPrefixConfig.Pattern
|
||||
@@ -318,14 +399,14 @@ func (gui *Gui) handleCommitPress() error {
|
||||
return gui.createErrorPanel(fmt.Sprintf("%s: %s", gui.Tr.LcCommitPrefixPatternError, err.Error()))
|
||||
}
|
||||
prefix := rgx.ReplaceAllString(gui.getCheckedOutBranch().Name, prefixReplace)
|
||||
gui.renderString("commitMessage", prefix)
|
||||
if err := commitMessageView.SetCursor(len(prefix), 0); err != nil {
|
||||
gui.renderString(gui.Views.CommitMessage, prefix)
|
||||
if err := gui.Views.CommitMessage.SetCursor(len(prefix), 0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
if err := gui.switchContext(gui.Contexts.CommitMessage.Context); err != nil {
|
||||
if err := gui.pushContext(gui.State.Contexts.CommitMessage); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -340,7 +421,7 @@ func (gui *Gui) promptToStageAllAndRetry(retry func() error) error {
|
||||
title: gui.Tr.NoFilesStagedTitle,
|
||||
prompt: gui.Tr.NoFilesStagedPrompt,
|
||||
handleConfirm: func() error {
|
||||
if err := gui.GitCommand.StageAll(); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.StageAllFiles).StageAll(); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
if err := gui.refreshFilesAndSubmodules(); err != nil {
|
||||
@@ -353,10 +434,12 @@ func (gui *Gui) promptToStageAllAndRetry(retry func() error) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleAmendCommitPress() error {
|
||||
if gui.State.FileManager.GetItemsLength() == 0 {
|
||||
return gui.createErrorPanel(gui.Tr.NoFilesStagedTitle)
|
||||
}
|
||||
|
||||
if len(gui.stagedFiles()) == 0 {
|
||||
return gui.promptToStageAllAndRetry(func() error {
|
||||
return gui.handleAmendCommitPress()
|
||||
})
|
||||
return gui.promptToStageAllAndRetry(gui.handleAmendCommitPress)
|
||||
}
|
||||
|
||||
if len(gui.State.Commits) == 0 {
|
||||
@@ -367,17 +450,9 @@ func (gui *Gui) handleAmendCommitPress() error {
|
||||
title: strings.Title(gui.Tr.AmendLastCommit),
|
||||
prompt: gui.Tr.SureToAmend,
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.AmendingStatus, func() error {
|
||||
ok, err := gui.runSyncOrAsyncCommand(gui.GitCommand.AmendHead())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
})
|
||||
cmdStr := gui.GitCommand.AmendHeadCmdStr()
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(cmdStr, gui.Tr.Spans.AmendCommit, true))
|
||||
return gui.withGpgHandling(cmdStr, gui.Tr.AmendingStatus, nil)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -385,86 +460,156 @@ func (gui *Gui) handleAmendCommitPress() error {
|
||||
// handleCommitEditorPress - handle when the user wants to commit changes via
|
||||
// their editor rather than via the popup panel
|
||||
func (gui *Gui) handleCommitEditorPress() error {
|
||||
if len(gui.stagedFiles()) == 0 {
|
||||
return gui.promptToStageAllAndRetry(func() error {
|
||||
return gui.handleCommitEditorPress()
|
||||
})
|
||||
if gui.State.FileManager.GetItemsLength() == 0 {
|
||||
return gui.createErrorPanel(gui.Tr.NoFilesStagedTitle)
|
||||
}
|
||||
|
||||
gui.PrepareSubProcess("git commit")
|
||||
return nil
|
||||
}
|
||||
if len(gui.stagedFiles()) == 0 {
|
||||
return gui.promptToStageAllAndRetry(gui.handleCommitEditorPress)
|
||||
}
|
||||
|
||||
// PrepareSubProcess - prepare a subprocess for execution and tell the gui to switch to it
|
||||
func (gui *Gui) PrepareSubProcess(command string) {
|
||||
splitCmd := str.ToArgv(command)
|
||||
gui.SubProcess = gui.OSCommand.PrepareSubProcess(splitCmd[0], splitCmd[1:]...)
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
return gui.Errors.ErrSubProcess
|
||||
})
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(
|
||||
gui.OSCommand.WithSpan(gui.Tr.Spans.Commit).PrepareSubProcess("git", "commit"),
|
||||
)
|
||||
}
|
||||
|
||||
func (gui *Gui) editFile(filename string) error {
|
||||
_, err := gui.runSyncOrAsyncCommand(gui.OSCommand.EditFile(filename))
|
||||
return err
|
||||
cmdStr, err := gui.GitCommand.EditFileCmdStr(filename)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(
|
||||
gui.OSCommand.WithSpan(gui.Tr.Spans.EditFile).PrepareShellSubProcess(cmdStr),
|
||||
)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleFileEdit(g *gocui.Gui, v *gocui.View) error {
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
func (gui *Gui) handleFileEdit() error {
|
||||
node := gui.getSelectedFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.editFile(file.Name)
|
||||
if node.File == nil {
|
||||
return gui.createErrorPanel(gui.Tr.ErrCannotEditDirectory)
|
||||
}
|
||||
|
||||
return gui.editFile(node.GetPath())
|
||||
}
|
||||
|
||||
func (gui *Gui) handleFileOpen(g *gocui.Gui, v *gocui.View) error {
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
func (gui *Gui) handleFileOpen() error {
|
||||
node := gui.getSelectedFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
return gui.openFile(file.Name)
|
||||
|
||||
return gui.openFile(node.GetPath())
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRefreshFiles(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{FILES}})
|
||||
func (gui *Gui) handleRefreshFiles() error {
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}})
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshStateFiles() error {
|
||||
state := gui.State
|
||||
|
||||
// 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()
|
||||
|
||||
selectedNode := gui.getSelectedFileNode()
|
||||
|
||||
prevNodes := gui.State.FileManager.GetAllItems()
|
||||
prevSelectedLineIdx := gui.State.Panels.Files.SelectedLineIdx
|
||||
|
||||
// get files to stage
|
||||
files := gui.GitCommand.GetStatusFiles(commands.GetStatusFileOptions{})
|
||||
gui.State.Files = gui.GitCommand.MergeStatusFiles(gui.State.Files, files, selectedFile)
|
||||
|
||||
// for when you stage the old file of a rename and the new file is in a collapsed dir
|
||||
state.FileManager.RWMutex.Lock()
|
||||
for _, file := range files {
|
||||
if selectedNode != nil && selectedNode.Path != "" && file.PreviousName == selectedNode.Path {
|
||||
state.FileManager.ExpandToPath(file.Name)
|
||||
}
|
||||
}
|
||||
|
||||
state.FileManager.SetFiles(files)
|
||||
state.FileManager.RWMutex.Unlock()
|
||||
|
||||
if err := gui.fileWatcher.addFilesToFileWatcher(files); err != nil {
|
||||
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
|
||||
if selectedNode != nil {
|
||||
newIdx := gui.findNewSelectedIdx(prevNodes[prevSelectedLineIdx:], state.FileManager.GetAllItems())
|
||||
if newIdx != -1 && newIdx != prevSelectedLineIdx {
|
||||
newNode := state.FileManager.GetItemAtIndex(newIdx)
|
||||
// when not in tree mode, we show merge conflict files at the top, so you
|
||||
// can work through them one by one without having to sift through a large
|
||||
// set of files. If you have just fixed the merge conflicts of a file, we
|
||||
// actually don't want to jump to that file's new position, because that
|
||||
// file will now be ages away amidst the other files without merge
|
||||
// conflicts: the user in this case would rather work on the next file
|
||||
// with merge conflicts, which will have moved up to fill the gap left by
|
||||
// the last file, meaning the cursor doesn't need to move at all.
|
||||
leaveCursor := !state.FileManager.InTreeMode() && newNode != nil &&
|
||||
selectedNode.File != nil && selectedNode.File.HasMergeConflicts &&
|
||||
newNode.File != nil && !newNode.File.HasMergeConflicts
|
||||
|
||||
if !leaveCursor {
|
||||
state.Panels.Files.SelectedLineIdx = newIdx
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gui.refreshSelectedLine(gui.State.Panels.Files, len(gui.State.Files))
|
||||
gui.refreshSelectedLine(state.Panels.Files, state.FileManager.GetItemsLength())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handlePullFiles(g *gocui.Gui, v *gocui.View) error {
|
||||
// Let's try to find our file again and move the cursor to that.
|
||||
// If we can't find our file, it was probably just removed by the user. In that
|
||||
// case, we go looking for where the next file has been moved to. Given that the
|
||||
// user could have removed a whole directory, we continue iterating through the old
|
||||
// nodes until we find one that exists in the new set of nodes, then move the cursor
|
||||
// to that.
|
||||
// prevNodes starts from our previously selected node because we don't need to consider anything above that
|
||||
func (gui *Gui) findNewSelectedIdx(prevNodes []*filetree.FileNode, currNodes []*filetree.FileNode) int {
|
||||
getPaths := func(node *filetree.FileNode) []string {
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
if node.File != nil && node.File.IsRename() {
|
||||
return node.File.Names()
|
||||
} else {
|
||||
return []string{node.Path}
|
||||
}
|
||||
}
|
||||
|
||||
for _, prevNode := range prevNodes {
|
||||
selectedPaths := getPaths(prevNode)
|
||||
|
||||
for idx, node := range currNodes {
|
||||
paths := getPaths(node)
|
||||
|
||||
// If you started off with a rename selected, and now it's broken in two, we want you to jump to the new file, not the old file.
|
||||
// This is because the new should be in the same position as the rename was meaning less cursor jumping
|
||||
foundOldFileInRename := prevNode.File != nil && prevNode.File.IsRename() && node.Path == prevNode.File.PreviousName
|
||||
foundNode := utils.StringArraysOverlap(paths, selectedPaths) && !foundOldFileInRename
|
||||
if foundNode {
|
||||
return idx
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func (gui *Gui) handlePullFiles() error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
span := gui.Tr.Spans.Pull
|
||||
|
||||
currentBranch := gui.currentBranch()
|
||||
if currentBranch == nil {
|
||||
// need to wait for branches to refresh
|
||||
@@ -480,38 +625,44 @@ func (gui *Gui) handlePullFiles(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
for branchName, branch := range conf.Branches {
|
||||
if branchName == currentBranch.Name {
|
||||
return gui.pullFiles(PullFilesOptions{RemoteName: branch.Remote, BranchName: branch.Name})
|
||||
return gui.pullFiles(PullFilesOptions{RemoteName: branch.Remote, BranchName: branch.Name, span: span})
|
||||
}
|
||||
}
|
||||
|
||||
return gui.prompt(gui.Tr.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.prompt(promptOpts{
|
||||
title: gui.Tr.EnterUpstream,
|
||||
initialContent: "origin/" + currentBranch.Name,
|
||||
handleConfirm: 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(errorMessage)
|
||||
}
|
||||
return gui.createErrorPanel(errorMessage)
|
||||
}
|
||||
return gui.pullFiles(PullFilesOptions{})
|
||||
return gui.pullFiles(PullFilesOptions{span: span})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return gui.pullFiles(PullFilesOptions{})
|
||||
return gui.pullFiles(PullFilesOptions{span: span})
|
||||
}
|
||||
|
||||
type PullFilesOptions struct {
|
||||
RemoteName string
|
||||
BranchName string
|
||||
span string
|
||||
}
|
||||
|
||||
func (gui *Gui) pullFiles(opts PullFilesOptions) error {
|
||||
if err := gui.createLoaderPanel(gui.g.CurrentView(), gui.Tr.PullWait); err != nil {
|
||||
if err := gui.createLoaderPanel(gui.Tr.PullWait); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mode := gui.Config.GetUserConfig().Git.Pull.Mode
|
||||
|
||||
go utils.Safe(func() { gui.pullWithMode(mode, opts) })
|
||||
// TODO: this doesn't look like a good idea. Why the goroutine?
|
||||
go utils.Safe(func() { _ = gui.pullWithMode(mode, opts) })
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -520,7 +671,9 @@ func (gui *Gui) pullWithMode(mode string, opts PullFilesOptions) error {
|
||||
gui.Mutexes.FetchMutex.Lock()
|
||||
defer gui.Mutexes.FetchMutex.Unlock()
|
||||
|
||||
err := gui.GitCommand.Fetch(
|
||||
gitCommand := gui.GitCommand.WithSpan(opts.span)
|
||||
|
||||
err := gitCommand.Fetch(
|
||||
commands.FetchOptions{
|
||||
PromptUserForCredential: gui.promptUserForCredential,
|
||||
RemoteName: opts.RemoteName,
|
||||
@@ -534,37 +687,37 @@ func (gui *Gui) pullWithMode(mode string, opts PullFilesOptions) error {
|
||||
|
||||
switch mode {
|
||||
case "rebase":
|
||||
err := gui.GitCommand.RebaseBranch("FETCH_HEAD")
|
||||
err := gitCommand.RebaseBranch("FETCH_HEAD")
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
case "merge":
|
||||
err := gui.GitCommand.Merge("FETCH_HEAD", commands.MergeOpts{})
|
||||
err := gitCommand.Merge("FETCH_HEAD", commands.MergeOpts{})
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
case "ff-only":
|
||||
err := gui.GitCommand.Merge("FETCH_HEAD", commands.MergeOpts{FastForwardOnly: true})
|
||||
err := 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.PushWait); err != nil {
|
||||
func (gui *Gui) pushWithForceFlag(force bool, upstream string, args string) error {
|
||||
if err := gui.createLoaderPanel(gui.Tr.PushWait); err != nil {
|
||||
return err
|
||||
}
|
||||
go utils.Safe(func() {
|
||||
branchName := gui.getCheckedOutBranch().Name
|
||||
err := gui.GitCommand.Push(branchName, force, upstream, args, gui.promptUserForCredential)
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.Push).Push(branchName, force, upstream, args, gui.promptUserForCredential)
|
||||
if err != nil && !force && strings.Contains(err.Error(), "Updates were rejected") {
|
||||
forcePushDisabled := gui.Config.GetUserConfig().Git.DisableForcePushing
|
||||
if forcePushDisabled {
|
||||
gui.createErrorPanel(gui.Tr.UpdatesRejectedAndForcePushDisabled)
|
||||
_ = gui.createErrorPanel(gui.Tr.UpdatesRejectedAndForcePushDisabled)
|
||||
return
|
||||
}
|
||||
gui.ask(askOpts{
|
||||
_ = gui.ask(askOpts{
|
||||
title: gui.Tr.ForcePush,
|
||||
prompt: gui.Tr.ForcePushPrompt,
|
||||
handleConfirm: func() error {
|
||||
return gui.pushWithForceFlag(v, true, upstream, args)
|
||||
return gui.pushWithForceFlag(true, upstream, args)
|
||||
},
|
||||
})
|
||||
return
|
||||
@@ -575,13 +728,17 @@ func (gui *Gui) pushWithForceFlag(v *gocui.View, force bool, upstream string, ar
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) pushFiles() error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// if we have pullables we'll ask if the user wants to force push
|
||||
currentBranch := gui.currentBranch()
|
||||
if currentBranch == nil {
|
||||
// need to wait for branches to refresh
|
||||
return nil
|
||||
}
|
||||
|
||||
if currentBranch.Pullables == "?" {
|
||||
// see if we have this branch in our config with an upstream
|
||||
@@ -591,19 +748,23 @@ func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
for branchName, branch := range conf.Branches {
|
||||
if branchName == currentBranch.Name {
|
||||
return gui.pushWithForceFlag(v, false, "", fmt.Sprintf("%s %s", branch.Remote, branchName))
|
||||
return gui.pushWithForceFlag(false, "", fmt.Sprintf("%s %s", branch.Remote, branchName))
|
||||
}
|
||||
}
|
||||
|
||||
if gui.GitCommand.PushToCurrent {
|
||||
return gui.pushWithForceFlag(v, false, "", "--set-upstream")
|
||||
return gui.pushWithForceFlag(false, "", "--set-upstream")
|
||||
} else {
|
||||
return gui.prompt(gui.Tr.EnterUpstream, "origin "+currentBranch.Name, func(response string) error {
|
||||
return gui.pushWithForceFlag(v, false, response, "")
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.EnterUpstream,
|
||||
initialContent: "origin " + currentBranch.Name,
|
||||
handleConfirm: func(response string) error {
|
||||
return gui.pushWithForceFlag(false, response, "")
|
||||
},
|
||||
})
|
||||
}
|
||||
} else if currentBranch.Pullables == "0" {
|
||||
return gui.pushWithForceFlag(v, false, "", "")
|
||||
return gui.pushWithForceFlag(false, "", "")
|
||||
}
|
||||
|
||||
forcePushDisabled := gui.Config.GetUserConfig().Git.DisableForcePushing
|
||||
@@ -615,7 +776,7 @@ func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error {
|
||||
title: gui.Tr.ForcePush,
|
||||
prompt: gui.Tr.ForcePushPrompt,
|
||||
handleConfirm: func() error {
|
||||
return gui.pushWithForceFlag(v, true, "", "")
|
||||
return gui.pushWithForceFlag(true, "", "")
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -630,18 +791,18 @@ func (gui *Gui) handleSwitchToMerge() error {
|
||||
return gui.createErrorPanel(gui.Tr.FileNoMergeCons)
|
||||
}
|
||||
|
||||
return gui.switchContext(gui.Contexts.Merging.Context)
|
||||
return gui.pushContext(gui.State.Contexts.Merging)
|
||||
}
|
||||
|
||||
func (gui *Gui) openFile(filename string) error {
|
||||
if err := gui.OSCommand.OpenFile(filename); err != nil {
|
||||
if err := gui.OSCommand.WithSpan(gui.Tr.Spans.OpenFile).OpenFile(filename); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) anyFilesWithMergeConflicts() bool {
|
||||
for _, file := range gui.State.Files {
|
||||
for _, file := range gui.State.FileManager.GetAllFiles() {
|
||||
if file.HasMergeConflicts {
|
||||
return true
|
||||
}
|
||||
@@ -649,25 +810,30 @@ func (gui *Gui) anyFilesWithMergeConflicts() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCustomCommand(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.prompt(gui.Tr.CustomCommand, "", func(command string) error {
|
||||
gui.SubProcess = gui.OSCommand.RunCustomCommand(command)
|
||||
return gui.Errors.ErrSubProcess
|
||||
func (gui *Gui) handleCustomCommand() error {
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.CustomCommand,
|
||||
handleConfirm: func(command string) error {
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(command, gui.Tr.Spans.CustomCommand, true))
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(
|
||||
gui.OSCommand.PrepareShellSubProcess(command),
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateStashMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCreateStashMenu() error {
|
||||
menuItems := []*menuItem{
|
||||
{
|
||||
displayString: gui.Tr.LcStashAllChanges,
|
||||
onPress: func() error {
|
||||
return gui.handleStashSave(gui.GitCommand.StashSave)
|
||||
return gui.handleStashSave(gui.GitCommand.WithSpan(gui.Tr.Spans.StashAllChanges).StashSave)
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: gui.Tr.LcStashStagedChanges,
|
||||
onPress: func() error {
|
||||
return gui.handleStashSave(gui.GitCommand.StashSaveStagedChanges)
|
||||
return gui.handleStashSave(gui.GitCommand.WithSpan(gui.Tr.Spans.StashStagedChanges).StashSaveStagedChanges)
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -675,10 +841,64 @@ func (gui *Gui) handleCreateStashMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.createMenu(gui.Tr.LcStashOptions, menuItems, createMenuOptions{showCancel: true})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStashChanges(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleStashChanges() error {
|
||||
return gui.handleStashSave(gui.GitCommand.StashSave)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateResetToUpstreamMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCreateResetToUpstreamMenu() error {
|
||||
return gui.createResetMenu("@{upstream}")
|
||||
}
|
||||
|
||||
func (gui *Gui) handleToggleDirCollapsed() error {
|
||||
node := gui.getSelectedFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.State.FileManager.ToggleCollapsed(node.GetPath())
|
||||
|
||||
if err := gui.postRefreshUpdate(gui.State.Contexts.Files); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleToggleFileTreeView() error {
|
||||
// get path of currently selected file
|
||||
path := gui.getSelectedPath()
|
||||
|
||||
gui.State.FileManager.ToggleShowTree()
|
||||
|
||||
// find that same node in the new format and move the cursor to it
|
||||
if path != "" {
|
||||
gui.State.FileManager.ExpandToPath(path)
|
||||
index, found := gui.State.FileManager.GetIndexForPath(path)
|
||||
if found {
|
||||
gui.filesListContext().GetPanelState().SetSelectedLineIdx(index)
|
||||
}
|
||||
}
|
||||
|
||||
if ContextKey(gui.Views.Files.Context) == FILES_CONTEXT_KEY {
|
||||
if err := gui.State.Contexts.Files.HandleRender(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.State.Contexts.Files.HandleFocus(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleOpenMergeTool() error {
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.MergeToolTitle,
|
||||
prompt: gui.Tr.MergeToolPrompt,
|
||||
handleConfirm: func() error {
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(
|
||||
gui.OSCommand.ExecutableFromString(gui.GitCommand.OpenMergeToolCmd()),
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
140
pkg/gui/filetree/build_tree.go
Normal file
140
pkg/gui/filetree/build_tree.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
)
|
||||
|
||||
func BuildTreeFromFiles(files []*models.File) *FileNode {
|
||||
root := &FileNode{}
|
||||
|
||||
var curr *FileNode
|
||||
for _, file := range files {
|
||||
splitPath := split(file.Name)
|
||||
curr = root
|
||||
outer:
|
||||
for i := range splitPath {
|
||||
var setFile *models.File
|
||||
isFile := i == len(splitPath)-1
|
||||
if isFile {
|
||||
setFile = file
|
||||
}
|
||||
|
||||
path := join(splitPath[:i+1])
|
||||
for _, existingChild := range curr.Children {
|
||||
if existingChild.Path == path {
|
||||
curr = existingChild
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
|
||||
newChild := &FileNode{
|
||||
Path: path,
|
||||
File: setFile,
|
||||
}
|
||||
curr.Children = append(curr.Children, newChild)
|
||||
|
||||
curr = newChild
|
||||
}
|
||||
}
|
||||
|
||||
root.Sort()
|
||||
root.Compress()
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
func BuildFlatTreeFromCommitFiles(files []*models.CommitFile) *CommitFileNode {
|
||||
rootAux := BuildTreeFromCommitFiles(files)
|
||||
sortedFiles := rootAux.GetLeaves()
|
||||
|
||||
return &CommitFileNode{Children: sortedFiles}
|
||||
}
|
||||
|
||||
func BuildTreeFromCommitFiles(files []*models.CommitFile) *CommitFileNode {
|
||||
root := &CommitFileNode{}
|
||||
|
||||
var curr *CommitFileNode
|
||||
for _, file := range files {
|
||||
splitPath := split(file.Name)
|
||||
curr = root
|
||||
outer:
|
||||
for i := range splitPath {
|
||||
var setFile *models.CommitFile
|
||||
isFile := i == len(splitPath)-1
|
||||
if isFile {
|
||||
setFile = file
|
||||
}
|
||||
|
||||
path := join(splitPath[:i+1])
|
||||
|
||||
for _, existingChild := range curr.Children {
|
||||
if existingChild.Path == path {
|
||||
curr = existingChild
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
|
||||
newChild := &CommitFileNode{
|
||||
Path: path,
|
||||
File: setFile,
|
||||
}
|
||||
curr.Children = append(curr.Children, newChild)
|
||||
|
||||
curr = newChild
|
||||
}
|
||||
}
|
||||
|
||||
root.Sort()
|
||||
root.Compress()
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
func BuildFlatTreeFromFiles(files []*models.File) *FileNode {
|
||||
rootAux := BuildTreeFromFiles(files)
|
||||
sortedFiles := rootAux.GetLeaves()
|
||||
|
||||
// from top down we have merge conflict files, then tracked file, then untracked
|
||||
// files. This is the one way in which sorting differs between flat mode and
|
||||
// tree mode
|
||||
sort.SliceStable(sortedFiles, func(i, j int) bool {
|
||||
iFile := sortedFiles[i].File
|
||||
jFile := sortedFiles[j].File
|
||||
|
||||
// never going to happen but just to be safe
|
||||
if iFile == nil || jFile == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if iFile.HasMergeConflicts && !jFile.HasMergeConflicts {
|
||||
return true
|
||||
}
|
||||
|
||||
if jFile.HasMergeConflicts && !iFile.HasMergeConflicts {
|
||||
return false
|
||||
}
|
||||
|
||||
if iFile.Tracked && !jFile.Tracked {
|
||||
return true
|
||||
}
|
||||
|
||||
if jFile.Tracked && !iFile.Tracked {
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
return &FileNode{Children: sortedFiles}
|
||||
}
|
||||
|
||||
func split(str string) []string {
|
||||
return strings.Split(str, "/")
|
||||
}
|
||||
|
||||
func join(strs []string) string {
|
||||
return strings.Join(strs, "/")
|
||||
}
|
||||
510
pkg/gui/filetree/build_tree_test.go
Normal file
510
pkg/gui/filetree/build_tree_test.go
Normal file
@@ -0,0 +1,510 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBuildTreeFromFiles(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
files []*models.File
|
||||
expected *FileNode
|
||||
}{
|
||||
{
|
||||
name: "no files",
|
||||
files: []*models.File{},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "files in same directory",
|
||||
files: []*models.File{
|
||||
{
|
||||
Name: "dir1/a",
|
||||
},
|
||||
{
|
||||
Name: "dir1/b",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
Path: "dir1",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "dir1/b"},
|
||||
Path: "dir1/b",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be compressed",
|
||||
files: []*models.File{
|
||||
{
|
||||
Name: "dir1/a",
|
||||
},
|
||||
{
|
||||
Name: "dir2/b",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
CompressionLevel: 1,
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "dir2/b"},
|
||||
Path: "dir2/b",
|
||||
CompressionLevel: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be sorted",
|
||||
files: []*models.File{
|
||||
{
|
||||
Name: "b",
|
||||
},
|
||||
{
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "a"},
|
||||
Path: "a",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "b"},
|
||||
Path: "b",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be sorted including a merge conflict file",
|
||||
files: []*models.File{
|
||||
{
|
||||
Name: "b",
|
||||
},
|
||||
{
|
||||
Name: "z",
|
||||
HasMergeConflicts: true,
|
||||
},
|
||||
{
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
// it is a little strange that we're not bubbling up our merge conflict
|
||||
// here but we are technically still in in tree mode and that's the rule
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "a"},
|
||||
Path: "a",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "b"},
|
||||
Path: "b",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "z", HasMergeConflicts: true},
|
||||
Path: "z",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
result := BuildTreeFromFiles(s.files)
|
||||
assert.EqualValues(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFlatTreeFromFiles(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
files []*models.File
|
||||
expected *FileNode
|
||||
}{
|
||||
{
|
||||
name: "no files",
|
||||
files: []*models.File{},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "files in same directory",
|
||||
files: []*models.File{
|
||||
{
|
||||
Name: "dir1/a",
|
||||
},
|
||||
{
|
||||
Name: "dir1/b",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
CompressionLevel: 0,
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "dir1/b"},
|
||||
Path: "dir1/b",
|
||||
CompressionLevel: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be compressed",
|
||||
files: []*models.File{
|
||||
{
|
||||
Name: "dir1/a",
|
||||
},
|
||||
{
|
||||
Name: "dir2/b",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
CompressionLevel: 1,
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "dir2/b"},
|
||||
Path: "dir2/b",
|
||||
CompressionLevel: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be sorted",
|
||||
files: []*models.File{
|
||||
{
|
||||
Name: "b",
|
||||
},
|
||||
{
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "a"},
|
||||
Path: "a",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "b"},
|
||||
Path: "b",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tracked, untracked, and conflicted files",
|
||||
files: []*models.File{
|
||||
{
|
||||
Name: "a2",
|
||||
Tracked: false,
|
||||
},
|
||||
{
|
||||
Name: "a1",
|
||||
Tracked: false,
|
||||
},
|
||||
{
|
||||
Name: "c2",
|
||||
HasMergeConflicts: true,
|
||||
},
|
||||
{
|
||||
Name: "c1",
|
||||
HasMergeConflicts: true,
|
||||
},
|
||||
{
|
||||
Name: "b2",
|
||||
Tracked: true,
|
||||
},
|
||||
{
|
||||
Name: "b1",
|
||||
Tracked: true,
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "c1", HasMergeConflicts: true},
|
||||
Path: "c1",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "c2", HasMergeConflicts: true},
|
||||
Path: "c2",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "b1", Tracked: true},
|
||||
Path: "b1",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "b2", Tracked: true},
|
||||
Path: "b2",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "a1", Tracked: false},
|
||||
Path: "a1",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "a2", Tracked: false},
|
||||
Path: "a2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
result := BuildFlatTreeFromFiles(s.files)
|
||||
assert.EqualValues(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildTreeFromCommitFiles(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
files []*models.CommitFile
|
||||
expected *CommitFileNode
|
||||
}{
|
||||
{
|
||||
name: "no files",
|
||||
files: []*models.CommitFile{},
|
||||
expected: &CommitFileNode{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "files in same directory",
|
||||
files: []*models.CommitFile{
|
||||
{
|
||||
Name: "dir1/a",
|
||||
},
|
||||
{
|
||||
Name: "dir1/b",
|
||||
},
|
||||
},
|
||||
expected: &CommitFileNode{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{
|
||||
{
|
||||
Path: "dir1",
|
||||
Children: []*CommitFileNode{
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
},
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir1/b"},
|
||||
Path: "dir1/b",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be compressed",
|
||||
files: []*models.CommitFile{
|
||||
{
|
||||
Name: "dir1/a",
|
||||
},
|
||||
{
|
||||
Name: "dir2/b",
|
||||
},
|
||||
},
|
||||
expected: &CommitFileNode{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
CompressionLevel: 1,
|
||||
},
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir2/b"},
|
||||
Path: "dir2/b",
|
||||
CompressionLevel: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be sorted",
|
||||
files: []*models.CommitFile{
|
||||
{
|
||||
Name: "b",
|
||||
},
|
||||
{
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
expected: &CommitFileNode{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{
|
||||
{
|
||||
File: &models.CommitFile{Name: "a"},
|
||||
Path: "a",
|
||||
},
|
||||
{
|
||||
File: &models.CommitFile{Name: "b"},
|
||||
Path: "b",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
result := BuildTreeFromCommitFiles(s.files)
|
||||
assert.EqualValues(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFlatTreeFromCommitFiles(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
files []*models.CommitFile
|
||||
expected *CommitFileNode
|
||||
}{
|
||||
{
|
||||
name: "no files",
|
||||
files: []*models.CommitFile{},
|
||||
expected: &CommitFileNode{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "files in same directory",
|
||||
files: []*models.CommitFile{
|
||||
{
|
||||
Name: "dir1/a",
|
||||
},
|
||||
{
|
||||
Name: "dir1/b",
|
||||
},
|
||||
},
|
||||
expected: &CommitFileNode{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
CompressionLevel: 0,
|
||||
},
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir1/b"},
|
||||
Path: "dir1/b",
|
||||
CompressionLevel: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be compressed",
|
||||
files: []*models.CommitFile{
|
||||
{
|
||||
Name: "dir1/a",
|
||||
},
|
||||
{
|
||||
Name: "dir2/b",
|
||||
},
|
||||
},
|
||||
expected: &CommitFileNode{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
CompressionLevel: 1,
|
||||
},
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir2/b"},
|
||||
Path: "dir2/b",
|
||||
CompressionLevel: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be sorted",
|
||||
files: []*models.CommitFile{
|
||||
{
|
||||
Name: "b",
|
||||
},
|
||||
{
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
expected: &CommitFileNode{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{
|
||||
{
|
||||
File: &models.CommitFile{Name: "a"},
|
||||
Path: "a",
|
||||
},
|
||||
{
|
||||
File: &models.CommitFile{Name: "b"},
|
||||
Path: "b",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
result := BuildFlatTreeFromCommitFiles(s.files)
|
||||
assert.EqualValues(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
20
pkg/gui/filetree/collapsed_paths.go
Normal file
20
pkg/gui/filetree/collapsed_paths.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package filetree
|
||||
|
||||
type CollapsedPaths map[string]bool
|
||||
|
||||
func (cp CollapsedPaths) ExpandToPath(path string) {
|
||||
// need every directory along the way
|
||||
splitPath := split(path)
|
||||
for i := range splitPath {
|
||||
dir := join(splitPath[0 : i+1])
|
||||
cp[dir] = false
|
||||
}
|
||||
}
|
||||
|
||||
func (cp CollapsedPaths) IsCollapsed(path string) bool {
|
||||
return cp[path]
|
||||
}
|
||||
|
||||
func (cp CollapsedPaths) ToggleCollapsed(path string) {
|
||||
cp[path] = !cp[path]
|
||||
}
|
||||
119
pkg/gui/filetree/commit_file_manager.go
Normal file
119
pkg/gui/filetree/commit_file_manager.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type CommitFileManager struct {
|
||||
files []*models.CommitFile
|
||||
tree *CommitFileNode
|
||||
showTree bool
|
||||
log *logrus.Entry
|
||||
collapsedPaths CollapsedPaths
|
||||
// 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
|
||||
}
|
||||
|
||||
func (m *CommitFileManager) GetParent() string {
|
||||
return m.parent
|
||||
}
|
||||
|
||||
func NewCommitFileManager(files []*models.CommitFile, log *logrus.Entry, showTree bool) *CommitFileManager {
|
||||
return &CommitFileManager{
|
||||
files: files,
|
||||
log: log,
|
||||
showTree: showTree,
|
||||
collapsedPaths: CollapsedPaths{},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *CommitFileManager) ExpandToPath(path string) {
|
||||
m.collapsedPaths.ExpandToPath(path)
|
||||
}
|
||||
|
||||
func (m *CommitFileManager) ToggleShowTree() {
|
||||
m.showTree = !m.showTree
|
||||
m.SetTree()
|
||||
}
|
||||
|
||||
func (m *CommitFileManager) GetItemAtIndex(index int) *CommitFileNode {
|
||||
// need to traverse the three depth first until we get to the index.
|
||||
return m.tree.GetNodeAtIndex(index+1, m.collapsedPaths) // ignoring root
|
||||
}
|
||||
|
||||
func (m *CommitFileManager) GetIndexForPath(path string) (int, bool) {
|
||||
index, found := m.tree.GetIndexForPath(path, m.collapsedPaths)
|
||||
return index - 1, found
|
||||
}
|
||||
|
||||
func (m *CommitFileManager) GetAllItems() []*CommitFileNode {
|
||||
if m.tree == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return m.tree.Flatten(m.collapsedPaths)[1:] // ignoring root
|
||||
}
|
||||
|
||||
func (m *CommitFileManager) GetItemsLength() int {
|
||||
return m.tree.Size(m.collapsedPaths) - 1 // ignoring root
|
||||
}
|
||||
|
||||
func (m *CommitFileManager) GetAllFiles() []*models.CommitFile {
|
||||
return m.files
|
||||
}
|
||||
|
||||
func (m *CommitFileManager) SetFiles(files []*models.CommitFile, parent string) {
|
||||
m.files = files
|
||||
m.parent = parent
|
||||
|
||||
m.SetTree()
|
||||
}
|
||||
|
||||
func (m *CommitFileManager) SetTree() {
|
||||
if m.showTree {
|
||||
m.tree = BuildTreeFromCommitFiles(m.files)
|
||||
} else {
|
||||
m.tree = BuildFlatTreeFromCommitFiles(m.files)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *CommitFileManager) IsCollapsed(path string) bool {
|
||||
return m.collapsedPaths.IsCollapsed(path)
|
||||
}
|
||||
|
||||
func (m *CommitFileManager) ToggleCollapsed(path string) {
|
||||
m.collapsedPaths.ToggleCollapsed(path)
|
||||
}
|
||||
|
||||
func (m *CommitFileManager) Render(diffName string, patchManager *patch.PatchManager) []string {
|
||||
// can't rely on renderAux to check for nil because an interface won't be nil if its concrete value is nil
|
||||
if m.tree == nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
return renderAux(m.tree, m.collapsedPaths, "", -1, func(n INode, depth int) string {
|
||||
castN := n.(*CommitFileNode)
|
||||
|
||||
// This is a little convoluted because we're dealing with either a leaf or a non-leaf.
|
||||
// But this code actually applies to both. If it's a leaf, the status will just
|
||||
// be whatever status it is, but if it's a non-leaf it will determine its status
|
||||
// based on the leaves of that subtree
|
||||
var status patch.PatchStatus
|
||||
if castN.EveryFile(func(file *models.CommitFile) bool {
|
||||
return patchManager.GetFileStatus(file.Name, m.parent) == patch.WHOLE
|
||||
}) {
|
||||
status = patch.WHOLE
|
||||
} else if castN.EveryFile(func(file *models.CommitFile) bool {
|
||||
return patchManager.GetFileStatus(file.Name, m.parent) == patch.UNSELECTED
|
||||
}) {
|
||||
status = patch.UNSELECTED
|
||||
} else {
|
||||
status = patch.PART
|
||||
}
|
||||
|
||||
return presentation.GetCommitFileLine(castN.NameAtDepth(depth), diffName, castN.File, status)
|
||||
})
|
||||
}
|
||||
172
pkg/gui/filetree/commit_file_node.go
Normal file
172
pkg/gui/filetree/commit_file_node.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
)
|
||||
|
||||
type CommitFileNode struct {
|
||||
Children []*CommitFileNode
|
||||
File *models.CommitFile
|
||||
Path string // e.g. '/path/to/mydir'
|
||||
CompressionLevel int // equal to the number of forward slashes you'll see in the path when it's rendered in tree mode
|
||||
}
|
||||
|
||||
// methods satisfying ListItem interface
|
||||
|
||||
func (s *CommitFileNode) ID() string {
|
||||
return s.GetPath()
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) Description() string {
|
||||
return s.GetPath()
|
||||
}
|
||||
|
||||
// methods satisfying INode interface
|
||||
|
||||
func (s *CommitFileNode) IsLeaf() bool {
|
||||
return s.File != nil
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) GetPath() string {
|
||||
return s.Path
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) GetChildren() []INode {
|
||||
result := make([]INode, len(s.Children))
|
||||
for i, child := range s.Children {
|
||||
result[i] = child
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) SetChildren(children []INode) {
|
||||
castChildren := make([]*CommitFileNode, len(children))
|
||||
for i, child := range children {
|
||||
castChildren[i] = child.(*CommitFileNode)
|
||||
}
|
||||
|
||||
s.Children = castChildren
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) GetCompressionLevel() int {
|
||||
return s.CompressionLevel
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) SetCompressionLevel(level int) {
|
||||
s.CompressionLevel = level
|
||||
}
|
||||
|
||||
// methods utilising generic functions for INodes
|
||||
|
||||
func (s *CommitFileNode) Sort() {
|
||||
sortNode(s)
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) ForEachFile(cb func(*models.CommitFile) error) error {
|
||||
return forEachLeaf(s, func(n INode) error {
|
||||
castNode := n.(*CommitFileNode)
|
||||
return cb(castNode.File)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) Any(test func(node *CommitFileNode) bool) bool {
|
||||
return any(s, func(n INode) bool {
|
||||
castNode := n.(*CommitFileNode)
|
||||
return test(castNode)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) Every(test func(node *CommitFileNode) bool) bool {
|
||||
return every(s, func(n INode) bool {
|
||||
castNode := n.(*CommitFileNode)
|
||||
return test(castNode)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) EveryFile(test func(file *models.CommitFile) bool) bool {
|
||||
return every(s, func(n INode) bool {
|
||||
castNode := n.(*CommitFileNode)
|
||||
|
||||
return castNode.File == nil || test(castNode.File)
|
||||
})
|
||||
}
|
||||
|
||||
func (n *CommitFileNode) Flatten(collapsedPaths map[string]bool) []*CommitFileNode {
|
||||
results := flatten(n, collapsedPaths)
|
||||
nodes := make([]*CommitFileNode, len(results))
|
||||
for i, result := range results {
|
||||
nodes[i] = result.(*CommitFileNode)
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
func (node *CommitFileNode) GetNodeAtIndex(index int, collapsedPaths map[string]bool) *CommitFileNode {
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := getNodeAtIndex(node, index, collapsedPaths)
|
||||
if result == nil {
|
||||
// not sure how this can be nil: we probably are missing a mutex somewhere
|
||||
return nil
|
||||
}
|
||||
|
||||
return result.(*CommitFileNode)
|
||||
}
|
||||
|
||||
func (node *CommitFileNode) GetIndexForPath(path string, collapsedPaths map[string]bool) (int, bool) {
|
||||
return getIndexForPath(node, path, collapsedPaths)
|
||||
}
|
||||
|
||||
func (node *CommitFileNode) Size(collapsedPaths map[string]bool) int {
|
||||
if node == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return size(node, collapsedPaths)
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) Compress() {
|
||||
// with these functions I try to only have type conversion code on the actual struct,
|
||||
// but comparing interface values to nil is fraught with danger so I'm duplicating
|
||||
// that code here.
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
|
||||
compressAux(s)
|
||||
}
|
||||
|
||||
// This ignores the root
|
||||
func (node *CommitFileNode) GetPathsMatching(test func(*CommitFileNode) bool) []string {
|
||||
return getPathsMatching(node, func(n INode) bool {
|
||||
return test(n.(*CommitFileNode))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) GetLeaves() []*CommitFileNode {
|
||||
leaves := getLeaves(s)
|
||||
castLeaves := make([]*CommitFileNode, len(leaves))
|
||||
for i := range leaves {
|
||||
castLeaves[i] = leaves[i].(*CommitFileNode)
|
||||
}
|
||||
|
||||
return castLeaves
|
||||
}
|
||||
|
||||
// extra methods
|
||||
|
||||
func (s *CommitFileNode) AnyFile(test func(file *models.CommitFile) bool) bool {
|
||||
return s.Any(func(node *CommitFileNode) bool {
|
||||
return node.IsLeaf() && test(node.File)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) NameAtDepth(depth int) string {
|
||||
splitName := split(s.Path)
|
||||
name := join(splitName[depth:])
|
||||
|
||||
return name
|
||||
}
|
||||
9
pkg/gui/filetree/constants.go
Normal file
9
pkg/gui/filetree/constants.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package filetree
|
||||
|
||||
const EXPANDED_ARROW = "▼"
|
||||
const COLLAPSED_ARROW = "►"
|
||||
|
||||
const INNER_ITEM = "├─ "
|
||||
const LAST_ITEM = "└─ "
|
||||
const NESTED = "│ "
|
||||
const NOTHING = " "
|
||||
101
pkg/gui/filetree/file_manager.go
Normal file
101
pkg/gui/filetree/file_manager.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type FileManager struct {
|
||||
files []*models.File
|
||||
tree *FileNode
|
||||
showTree bool
|
||||
log *logrus.Entry
|
||||
collapsedPaths CollapsedPaths
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func NewFileManager(files []*models.File, log *logrus.Entry, showTree bool) *FileManager {
|
||||
return &FileManager{
|
||||
files: files,
|
||||
log: log,
|
||||
showTree: showTree,
|
||||
collapsedPaths: CollapsedPaths{},
|
||||
RWMutex: sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *FileManager) InTreeMode() bool {
|
||||
return m.showTree
|
||||
}
|
||||
|
||||
func (m *FileManager) ExpandToPath(path string) {
|
||||
m.collapsedPaths.ExpandToPath(path)
|
||||
}
|
||||
|
||||
func (m *FileManager) ToggleShowTree() {
|
||||
m.showTree = !m.showTree
|
||||
m.SetTree()
|
||||
}
|
||||
|
||||
func (m *FileManager) GetItemAtIndex(index int) *FileNode {
|
||||
// need to traverse the three depth first until we get to the index.
|
||||
return m.tree.GetNodeAtIndex(index+1, m.collapsedPaths) // ignoring root
|
||||
}
|
||||
|
||||
func (m *FileManager) GetIndexForPath(path string) (int, bool) {
|
||||
index, found := m.tree.GetIndexForPath(path, m.collapsedPaths)
|
||||
return index - 1, found
|
||||
}
|
||||
|
||||
func (m *FileManager) GetAllItems() []*FileNode {
|
||||
if m.tree == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return m.tree.Flatten(m.collapsedPaths)[1:] // ignoring root
|
||||
}
|
||||
|
||||
func (m *FileManager) GetItemsLength() int {
|
||||
return m.tree.Size(m.collapsedPaths) - 1 // ignoring root
|
||||
}
|
||||
|
||||
func (m *FileManager) GetAllFiles() []*models.File {
|
||||
return m.files
|
||||
}
|
||||
|
||||
func (m *FileManager) SetFiles(files []*models.File) {
|
||||
m.files = files
|
||||
|
||||
m.SetTree()
|
||||
}
|
||||
|
||||
func (m *FileManager) SetTree() {
|
||||
if m.showTree {
|
||||
m.tree = BuildTreeFromFiles(m.files)
|
||||
} else {
|
||||
m.tree = BuildFlatTreeFromFiles(m.files)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *FileManager) IsCollapsed(path string) bool {
|
||||
return m.collapsedPaths.IsCollapsed(path)
|
||||
}
|
||||
|
||||
func (m *FileManager) ToggleCollapsed(path string) {
|
||||
m.collapsedPaths.ToggleCollapsed(path)
|
||||
}
|
||||
|
||||
func (m *FileManager) Render(diffName string, submoduleConfigs []*models.SubmoduleConfig) []string {
|
||||
// can't rely on renderAux to check for nil because an interface won't be nil if its concrete value is nil
|
||||
if m.tree == nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
return renderAux(m.tree, m.collapsedPaths, "", -1, func(n INode, depth int) string {
|
||||
castN := n.(*FileNode)
|
||||
return presentation.GetFileLine(castN.GetHasUnstagedChanges(), castN.GetHasStagedChanges(), castN.NameAtDepth(depth), diffName, submoduleConfigs, castN.File)
|
||||
})
|
||||
}
|
||||
91
pkg/gui/filetree/file_manager_test.go
Normal file
91
pkg/gui/filetree/file_manager_test.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRender(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
root *FileNode
|
||||
collapsedPaths map[string]bool
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "nil node",
|
||||
root: nil,
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "leaf node",
|
||||
root: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{File: &models.File{Name: "test", ShortStatus: " M", HasStagedChanges: true}, Path: "test"},
|
||||
},
|
||||
},
|
||||
expected: []string{" M test"},
|
||||
},
|
||||
{
|
||||
name: "big example",
|
||||
root: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
Path: "dir1",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "dir1/file2", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "dir1/file2",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "dir1/file3", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "dir1/file3",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "dir2",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
Path: "dir2/dir2",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "dir2/dir2/file3", ShortStatus: " M", HasStagedChanges: true},
|
||||
Path: "dir2/dir2/file3",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "dir2/dir2/file4",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "dir2/file5", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "dir2/file5",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "file1",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{"dir1 ►", "dir2 ▼", "├─ dir2 ▼", "│ ├─ M file3", "│ └─ M file4", "└─ M file5", "M file1"},
|
||||
collapsedPaths: map[string]bool{"dir1": true},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
mngr := &FileManager{tree: s.root, collapsedPaths: s.collapsedPaths}
|
||||
result := mngr.Render("", nil)
|
||||
assert.EqualValues(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
189
pkg/gui/filetree/file_node.go
Normal file
189
pkg/gui/filetree/file_node.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
)
|
||||
|
||||
type FileNode struct {
|
||||
Children []*FileNode
|
||||
File *models.File
|
||||
Path string // e.g. '/path/to/mydir'
|
||||
CompressionLevel int // equal to the number of forward slashes you'll see in the path when it's rendered in tree mode
|
||||
}
|
||||
|
||||
// methods satisfying ListItem interface
|
||||
|
||||
func (s *FileNode) ID() string {
|
||||
return s.GetPath()
|
||||
}
|
||||
|
||||
func (s *FileNode) Description() string {
|
||||
return s.GetPath()
|
||||
}
|
||||
|
||||
// methods satisfying INode interface
|
||||
|
||||
func (s *FileNode) IsLeaf() bool {
|
||||
return s.File != nil
|
||||
}
|
||||
|
||||
func (s *FileNode) GetPath() string {
|
||||
return s.Path
|
||||
}
|
||||
|
||||
func (s *FileNode) GetChildren() []INode {
|
||||
result := make([]INode, len(s.Children))
|
||||
for i, child := range s.Children {
|
||||
result[i] = child
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *FileNode) SetChildren(children []INode) {
|
||||
castChildren := make([]*FileNode, len(children))
|
||||
for i, child := range children {
|
||||
castChildren[i] = child.(*FileNode)
|
||||
}
|
||||
|
||||
s.Children = castChildren
|
||||
}
|
||||
|
||||
func (s *FileNode) GetCompressionLevel() int {
|
||||
return s.CompressionLevel
|
||||
}
|
||||
|
||||
func (s *FileNode) SetCompressionLevel(level int) {
|
||||
s.CompressionLevel = level
|
||||
}
|
||||
|
||||
// methods utilising generic functions for INodes
|
||||
|
||||
func (s *FileNode) Sort() {
|
||||
sortNode(s)
|
||||
}
|
||||
|
||||
func (s *FileNode) ForEachFile(cb func(*models.File) error) error {
|
||||
return forEachLeaf(s, func(n INode) error {
|
||||
castNode := n.(*FileNode)
|
||||
return cb(castNode.File)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *FileNode) Any(test func(node *FileNode) bool) bool {
|
||||
return any(s, func(n INode) bool {
|
||||
castNode := n.(*FileNode)
|
||||
return test(castNode)
|
||||
})
|
||||
}
|
||||
|
||||
func (n *FileNode) Flatten(collapsedPaths map[string]bool) []*FileNode {
|
||||
results := flatten(n, collapsedPaths)
|
||||
nodes := make([]*FileNode, len(results))
|
||||
for i, result := range results {
|
||||
nodes[i] = result.(*FileNode)
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
func (node *FileNode) GetNodeAtIndex(index int, collapsedPaths map[string]bool) *FileNode {
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := getNodeAtIndex(node, index, collapsedPaths)
|
||||
if result == nil {
|
||||
// not sure how this can be nil: we probably are missing a mutex somewhere
|
||||
return nil
|
||||
}
|
||||
|
||||
return result.(*FileNode)
|
||||
}
|
||||
|
||||
func (node *FileNode) GetIndexForPath(path string, collapsedPaths map[string]bool) (int, bool) {
|
||||
return getIndexForPath(node, path, collapsedPaths)
|
||||
}
|
||||
|
||||
func (node *FileNode) Size(collapsedPaths map[string]bool) int {
|
||||
if node == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return size(node, collapsedPaths)
|
||||
}
|
||||
|
||||
func (s *FileNode) Compress() {
|
||||
// with these functions I try to only have type conversion code on the actual struct,
|
||||
// but comparing interface values to nil is fraught with danger so I'm duplicating
|
||||
// that code here.
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
|
||||
compressAux(s)
|
||||
}
|
||||
|
||||
// This ignores the root
|
||||
func (node *FileNode) GetPathsMatching(test func(*FileNode) bool) []string {
|
||||
return getPathsMatching(node, func(n INode) bool {
|
||||
return test(n.(*FileNode))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *FileNode) GetLeaves() []*FileNode {
|
||||
leaves := getLeaves(s)
|
||||
castLeaves := make([]*FileNode, len(leaves))
|
||||
for i := range leaves {
|
||||
castLeaves[i] = leaves[i].(*FileNode)
|
||||
}
|
||||
|
||||
return castLeaves
|
||||
}
|
||||
|
||||
// extra methods
|
||||
|
||||
func (s *FileNode) GetHasUnstagedChanges() bool {
|
||||
return s.AnyFile(func(file *models.File) bool { return file.HasUnstagedChanges })
|
||||
}
|
||||
|
||||
func (s *FileNode) GetHasStagedChanges() bool {
|
||||
return s.AnyFile(func(file *models.File) bool { return file.HasStagedChanges })
|
||||
}
|
||||
|
||||
func (s *FileNode) GetHasInlineMergeConflicts() bool {
|
||||
return s.AnyFile(func(file *models.File) bool { return file.HasInlineMergeConflicts })
|
||||
}
|
||||
|
||||
func (s *FileNode) GetIsTracked() bool {
|
||||
return s.AnyFile(func(file *models.File) bool { return file.Tracked })
|
||||
}
|
||||
|
||||
func (s *FileNode) AnyFile(test func(file *models.File) bool) bool {
|
||||
return s.Any(func(node *FileNode) bool {
|
||||
return node.IsLeaf() && test(node.File)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *FileNode) NameAtDepth(depth int) string {
|
||||
splitName := split(s.Path)
|
||||
name := join(splitName[depth:])
|
||||
|
||||
if s.File != nil && s.File.IsRename() {
|
||||
splitPrevName := split(s.File.PreviousName)
|
||||
|
||||
prevName := s.File.PreviousName
|
||||
// if the file has just been renamed inside the same directory, we can shave off
|
||||
// the prefix for the previous path too. Otherwise we'll keep it unchanged
|
||||
sameParentDir := len(splitName) == len(splitPrevName) && join(splitName[0:depth]) == join(splitPrevName[0:depth])
|
||||
if sameParentDir {
|
||||
prevName = join(splitPrevName[depth:])
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s%s%s", prevName, " → ", name)
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
125
pkg/gui/filetree/file_node_test.go
Normal file
125
pkg/gui/filetree/file_node_test.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCompress(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
root *FileNode
|
||||
expected *FileNode
|
||||
}{
|
||||
{
|
||||
name: "nil node",
|
||||
root: nil,
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "leaf node",
|
||||
root: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{File: &models.File{Name: "test", ShortStatus: " M", HasStagedChanges: true}, Path: "test"},
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{File: &models.File{Name: "test", ShortStatus: " M", HasStagedChanges: true}, Path: "test"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "big example",
|
||||
root: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
Path: "dir1",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "file2", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "dir1/file2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "dir2",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "file3", ShortStatus: " M", HasStagedChanges: true},
|
||||
Path: "dir2/file3",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "file4", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "dir2/file4",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "dir3",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
Path: "dir3/dir3-1",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "file5", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "dir3/dir3-1/file5",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "file1",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
Path: "dir1/file2",
|
||||
File: &models.File{Name: "file2", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
CompressionLevel: 1,
|
||||
},
|
||||
{
|
||||
Path: "dir2",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "file3", ShortStatus: " M", HasStagedChanges: true},
|
||||
Path: "dir2/file3",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "file4", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "dir2/file4",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "dir3/dir3-1/file5",
|
||||
File: &models.File{Name: "file5", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
CompressionLevel: 2,
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "file1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
s.root.Compress()
|
||||
assert.EqualValues(t, s.expected, s.root)
|
||||
})
|
||||
}
|
||||
}
|
||||
262
pkg/gui/filetree/inode.go
Normal file
262
pkg/gui/filetree/inode.go
Normal file
@@ -0,0 +1,262 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type INode interface {
|
||||
IsLeaf() bool
|
||||
GetPath() string
|
||||
GetChildren() []INode
|
||||
SetChildren([]INode)
|
||||
GetCompressionLevel() int
|
||||
SetCompressionLevel(int)
|
||||
}
|
||||
|
||||
func sortNode(node INode) {
|
||||
sortChildren(node)
|
||||
|
||||
for _, child := range node.GetChildren() {
|
||||
sortNode(child)
|
||||
}
|
||||
}
|
||||
|
||||
func sortChildren(node INode) {
|
||||
if node.IsLeaf() {
|
||||
return
|
||||
}
|
||||
|
||||
children := node.GetChildren()
|
||||
sortedChildren := make([]INode, len(children))
|
||||
copy(sortedChildren, children)
|
||||
|
||||
sort.Slice(sortedChildren, func(i, j int) bool {
|
||||
if !sortedChildren[i].IsLeaf() && sortedChildren[j].IsLeaf() {
|
||||
return true
|
||||
}
|
||||
if sortedChildren[i].IsLeaf() && !sortedChildren[j].IsLeaf() {
|
||||
return false
|
||||
}
|
||||
|
||||
return sortedChildren[i].GetPath() < sortedChildren[j].GetPath()
|
||||
})
|
||||
|
||||
// TODO: think about making this in-place
|
||||
node.SetChildren(sortedChildren)
|
||||
}
|
||||
|
||||
func forEachLeaf(node INode, cb func(INode) error) error {
|
||||
if node.IsLeaf() {
|
||||
if err := cb(node); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, child := range node.GetChildren() {
|
||||
if err := forEachLeaf(child, cb); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func any(node INode, test func(INode) bool) bool {
|
||||
if test(node) {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, child := range node.GetChildren() {
|
||||
if any(child, test) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func every(node INode, test func(INode) bool) bool {
|
||||
if !test(node) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, child := range node.GetChildren() {
|
||||
if !every(child, test) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func flatten(node INode, collapsedPaths map[string]bool) []INode {
|
||||
result := []INode{}
|
||||
result = append(result, node)
|
||||
|
||||
if !collapsedPaths[node.GetPath()] {
|
||||
for _, child := range node.GetChildren() {
|
||||
result = append(result, flatten(child, collapsedPaths)...)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func getNodeAtIndex(node INode, index int, collapsedPaths map[string]bool) INode {
|
||||
foundNode, _ := getNodeAtIndexAux(node, index, collapsedPaths)
|
||||
|
||||
return foundNode
|
||||
}
|
||||
|
||||
func getNodeAtIndexAux(node INode, index int, collapsedPaths map[string]bool) (INode, int) {
|
||||
offset := 1
|
||||
|
||||
if index == 0 {
|
||||
return node, offset
|
||||
}
|
||||
|
||||
if !collapsedPaths[node.GetPath()] {
|
||||
for _, child := range node.GetChildren() {
|
||||
foundNode, offsetChange := getNodeAtIndexAux(child, index-offset, collapsedPaths)
|
||||
offset += offsetChange
|
||||
if foundNode != nil {
|
||||
return foundNode, offset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, offset
|
||||
}
|
||||
|
||||
func getIndexForPath(node INode, path string, collapsedPaths map[string]bool) (int, bool) {
|
||||
offset := 0
|
||||
|
||||
if node.GetPath() == path {
|
||||
return offset, true
|
||||
}
|
||||
|
||||
if !collapsedPaths[node.GetPath()] {
|
||||
for _, child := range node.GetChildren() {
|
||||
offsetChange, found := getIndexForPath(child, path, collapsedPaths)
|
||||
offset += offsetChange + 1
|
||||
if found {
|
||||
return offset, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return offset, false
|
||||
}
|
||||
|
||||
func size(node INode, collapsedPaths map[string]bool) int {
|
||||
output := 1
|
||||
|
||||
if !collapsedPaths[node.GetPath()] {
|
||||
for _, child := range node.GetChildren() {
|
||||
output += size(child, collapsedPaths)
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func compressAux(node INode) INode {
|
||||
if node.IsLeaf() {
|
||||
return node
|
||||
}
|
||||
|
||||
children := node.GetChildren()
|
||||
for i := range children {
|
||||
grandchildren := children[i].GetChildren()
|
||||
for len(grandchildren) == 1 {
|
||||
grandchildren[0].SetCompressionLevel(children[i].GetCompressionLevel() + 1)
|
||||
children[i] = grandchildren[0]
|
||||
grandchildren = children[i].GetChildren()
|
||||
}
|
||||
}
|
||||
|
||||
for i := range children {
|
||||
children[i] = compressAux(children[i])
|
||||
}
|
||||
|
||||
node.SetChildren(children)
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
func getPathsMatching(node INode, test func(INode) bool) []string {
|
||||
paths := []string{}
|
||||
|
||||
if test(node) {
|
||||
paths = append(paths, node.GetPath())
|
||||
}
|
||||
|
||||
for _, child := range node.GetChildren() {
|
||||
paths = append(paths, getPathsMatching(child, test)...)
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
func getLeaves(node INode) []INode {
|
||||
if node.IsLeaf() {
|
||||
return []INode{node}
|
||||
}
|
||||
|
||||
output := []INode{}
|
||||
for _, child := range node.GetChildren() {
|
||||
output = append(output, getLeaves(child)...)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func renderAux(s INode, collapsedPaths CollapsedPaths, prefix string, depth int, renderLine func(INode, int) string) []string {
|
||||
isRoot := depth == -1
|
||||
|
||||
renderLineWithPrefix := func() string {
|
||||
return prefix + renderLine(s, depth)
|
||||
}
|
||||
|
||||
if s.IsLeaf() {
|
||||
if isRoot {
|
||||
return []string{}
|
||||
}
|
||||
return []string{renderLineWithPrefix()}
|
||||
}
|
||||
|
||||
if collapsedPaths.IsCollapsed(s.GetPath()) {
|
||||
return []string{fmt.Sprintf("%s %s", renderLineWithPrefix(), COLLAPSED_ARROW)}
|
||||
}
|
||||
|
||||
arr := []string{}
|
||||
if !isRoot {
|
||||
arr = append(arr, fmt.Sprintf("%s %s", renderLineWithPrefix(), EXPANDED_ARROW))
|
||||
}
|
||||
|
||||
newPrefix := prefix
|
||||
if strings.HasSuffix(prefix, LAST_ITEM) {
|
||||
newPrefix = strings.TrimSuffix(prefix, LAST_ITEM) + NOTHING
|
||||
} else if strings.HasSuffix(prefix, INNER_ITEM) {
|
||||
newPrefix = strings.TrimSuffix(prefix, INNER_ITEM) + NESTED
|
||||
}
|
||||
|
||||
for i, child := range s.GetChildren() {
|
||||
isLast := i == len(s.GetChildren())-1
|
||||
|
||||
var childPrefix string
|
||||
if isRoot {
|
||||
childPrefix = newPrefix
|
||||
} else if isLast {
|
||||
childPrefix = newPrefix + LAST_ITEM
|
||||
} else {
|
||||
childPrefix = newPrefix + INNER_ITEM
|
||||
}
|
||||
|
||||
arr = append(arr, renderAux(child, collapsedPaths, childPrefix, depth+1+s.GetCompressionLevel(), renderLine)...)
|
||||
}
|
||||
|
||||
return arr
|
||||
}
|
||||
@@ -3,11 +3,9 @@ package gui
|
||||
func (gui *Gui) validateNotInFilterMode() (bool, error) {
|
||||
if gui.State.Modes.Filtering.Active() {
|
||||
err := gui.ask(askOpts{
|
||||
title: gui.Tr.MustExitFilterModeTitle,
|
||||
prompt: gui.Tr.MustExitFilterModePrompt,
|
||||
handleConfirm: func() error {
|
||||
return gui.exitFilterMode()
|
||||
},
|
||||
title: gui.Tr.MustExitFilterModeTitle,
|
||||
prompt: gui.Tr.MustExitFilterModePrompt,
|
||||
handleConfirm: gui.exitFilterMode,
|
||||
})
|
||||
|
||||
return false, err
|
||||
@@ -16,6 +14,29 @@ func (gui *Gui) validateNotInFilterMode() (bool, error) {
|
||||
}
|
||||
|
||||
func (gui *Gui) exitFilterMode() error {
|
||||
gui.State.Modes.Filtering.Path = ""
|
||||
return gui.Errors.ErrRestart
|
||||
return gui.clearFiltering()
|
||||
}
|
||||
|
||||
func (gui *Gui) clearFiltering() error {
|
||||
gui.State.Modes.Filtering.Reset()
|
||||
if gui.State.ScreenMode == SCREEN_HALF {
|
||||
gui.State.ScreenMode = SCREEN_NORMAL
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{COMMITS}})
|
||||
}
|
||||
|
||||
func (gui *Gui) setFiltering(path string) error {
|
||||
gui.State.Modes.Filtering.SetPath(path)
|
||||
if gui.State.ScreenMode == SCREEN_NORMAL {
|
||||
gui.State.ScreenMode = SCREEN_HALF
|
||||
}
|
||||
|
||||
if err := gui.pushContext(gui.State.Contexts.BranchCommits); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{COMMITS}, then: func() {
|
||||
gui.State.Contexts.BranchCommits.GetPanelState().SetSelectedLineIdx(0)
|
||||
}})
|
||||
}
|
||||
|
||||
@@ -3,26 +3,20 @@ 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
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateFilteringMenuPanel() error {
|
||||
fileName := ""
|
||||
switch v.Name() {
|
||||
case "files":
|
||||
file := gui.getSelectedFile()
|
||||
if file != nil {
|
||||
fileName = file.Name
|
||||
switch gui.currentSideListContext() {
|
||||
case gui.State.Contexts.Files:
|
||||
node := gui.getSelectedFileNode()
|
||||
if node != nil {
|
||||
fileName = node.GetPath()
|
||||
}
|
||||
case "commitFiles":
|
||||
file := gui.getSelectedCommitFile()
|
||||
if file != nil {
|
||||
fileName = file.Name
|
||||
case gui.State.Contexts.CommitFiles:
|
||||
node := gui.getSelectedCommitFileNode()
|
||||
if node != nil {
|
||||
fileName = node.GetPath()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +26,7 @@ func (gui *Gui) handleCreateFilteringMenuPanel(g *gocui.Gui, v *gocui.View) erro
|
||||
menuItems = append(menuItems, &menuItem{
|
||||
displayString: fmt.Sprintf("%s '%s'", gui.Tr.LcFilterBy, fileName),
|
||||
onPress: func() error {
|
||||
gui.State.Modes.Filtering.Path = fileName
|
||||
return gui.Errors.ErrRestart
|
||||
return gui.setFiltering(fileName)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -41,9 +34,11 @@ func (gui *Gui) handleCreateFilteringMenuPanel(g *gocui.Gui, v *gocui.View) erro
|
||||
menuItems = append(menuItems, &menuItem{
|
||||
displayString: gui.Tr.LcFilterPathOption,
|
||||
onPress: func() error {
|
||||
return gui.prompt(gui.Tr.LcEnterFileName, "", func(response string) error {
|
||||
gui.State.Modes.Filtering.Path = strings.TrimSpace(response)
|
||||
return gui.Errors.ErrRestart
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.LcEnterFileName,
|
||||
handleConfirm: func(response string) error {
|
||||
return gui.setFiltering(strings.TrimSpace(response))
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -51,10 +46,7 @@ func (gui *Gui) handleCreateFilteringMenuPanel(g *gocui.Gui, v *gocui.View) erro
|
||||
if gui.State.Modes.Filtering.Active() {
|
||||
menuItems = append(menuItems, &menuItem{
|
||||
displayString: gui.Tr.LcExitFilterMode,
|
||||
onPress: func() error {
|
||||
gui.State.Modes.Filtering.Path = ""
|
||||
return gui.Errors.ErrRestart
|
||||
},
|
||||
onPress: gui.clearFiltering,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -32,19 +31,19 @@ func (gui *Gui) gitFlowFinishBranch(gitFlowConfig string, branchName string) err
|
||||
return gui.createErrorPanel(gui.Tr.NotAGitFlowBranch)
|
||||
}
|
||||
|
||||
subProcess := gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "finish", suffix)
|
||||
gui.SubProcess = subProcess
|
||||
return gui.Errors.ErrSubProcess
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(
|
||||
gui.OSCommand.WithSpan(gui.Tr.Spans.GitFlowFinish).PrepareSubProcess("git", "flow", branchType, "finish", suffix),
|
||||
)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateGitFlowMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCreateGitFlowMenu() error {
|
||||
branch := gui.getSelectedBranch()
|
||||
if branch == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// get config
|
||||
gitFlowConfig, err := gui.OSCommand.RunCommandWithOutput("git config --local --get-regexp gitflow")
|
||||
gitFlowConfig, err := gui.GitCommand.RunCommandWithOutput("git config --local --get-regexp gitflow")
|
||||
if err != nil {
|
||||
return gui.createErrorPanel("You need to install git-flow and enable it in this repo to use git-flow features")
|
||||
}
|
||||
@@ -52,10 +51,14 @@ func (gui *Gui) handleCreateGitFlowMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
startHandler := func(branchType string) func() error {
|
||||
return func() error {
|
||||
title := utils.ResolvePlaceholderString(gui.Tr.NewGitFlowBranchPrompt, map[string]string{"branchType": branchType})
|
||||
return gui.prompt(title, "", func(name string) error {
|
||||
subProcess := gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "start", name)
|
||||
gui.SubProcess = subProcess
|
||||
return gui.Errors.ErrSubProcess
|
||||
|
||||
return gui.prompt(promptOpts{
|
||||
title: title,
|
||||
handleConfirm: func(name string) error {
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(
|
||||
gui.OSCommand.WithSpan(gui.Tr.Spans.GitFlowStart).PrepareSubProcess("git", "flow", branchType, "start", name),
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
@@ -12,8 +13,8 @@ import (
|
||||
// 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 {
|
||||
for _, view := range []*gocui.View{gui.Views.Branches, gui.Views.Commits} {
|
||||
if err := gui.rerenderView(view); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -21,149 +22,172 @@ func (gui *Gui) rerenderViewsWithScreenModeDependentContent() error {
|
||||
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
|
||||
// TODO: GENERICS
|
||||
func nextIntInCycle(sl []WindowMaximisation, current WindowMaximisation) WindowMaximisation {
|
||||
for i, val := range sl {
|
||||
if val == current {
|
||||
if i == len(sl)-1 {
|
||||
return sl[0]
|
||||
}
|
||||
return sl[i+1]
|
||||
}
|
||||
}
|
||||
ox, oy := mainView.Origin()
|
||||
return sl[0]
|
||||
}
|
||||
|
||||
// TODO: GENERICS
|
||||
func prevIntInCycle(sl []WindowMaximisation, current WindowMaximisation) WindowMaximisation {
|
||||
for i, val := range sl {
|
||||
if val == current {
|
||||
if i > 0 {
|
||||
return sl[i-1]
|
||||
}
|
||||
return sl[len(sl)-1]
|
||||
}
|
||||
}
|
||||
return sl[len(sl)-1]
|
||||
}
|
||||
|
||||
func (gui *Gui) nextScreenMode() error {
|
||||
gui.State.ScreenMode = nextIntInCycle([]WindowMaximisation{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode)
|
||||
|
||||
return gui.rerenderViewsWithScreenModeDependentContent()
|
||||
}
|
||||
|
||||
func (gui *Gui) prevScreenMode() error {
|
||||
gui.State.ScreenMode = prevIntInCycle([]WindowMaximisation{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode)
|
||||
|
||||
return gui.rerenderViewsWithScreenModeDependentContent()
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollUpView(view *gocui.View) error {
|
||||
ox, oy := view.Origin()
|
||||
newOy := int(math.Max(0, float64(oy-gui.Config.GetUserConfig().Gui.ScrollHeight)))
|
||||
return mainView.SetOrigin(ox, newOy)
|
||||
return view.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().Gui.ScrollPastBottom {
|
||||
_, sy := mainView.Size()
|
||||
y += sy
|
||||
}
|
||||
scrollHeight := gui.Config.GetUserConfig().Gui.ScrollHeight
|
||||
if y < mainView.LinesHeight() {
|
||||
if err := mainView.SetOrigin(ox, oy+scrollHeight); err != nil {
|
||||
func (gui *Gui) scrollDownView(view *gocui.View) error {
|
||||
ox, oy := view.Origin()
|
||||
scrollHeight := gui.linesToScrollDown(view)
|
||||
if scrollHeight > 0 {
|
||||
if err := view.SetOrigin(ox, oy+scrollHeight); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if manager, ok := gui.viewBufferManagerMap[viewName]; ok {
|
||||
|
||||
if manager, ok := gui.viewBufferManagerMap[view.Name()]; ok {
|
||||
manager.ReadLines(scrollHeight)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollUpMain(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) linesToScrollDown(view *gocui.View) int {
|
||||
_, oy := view.Origin()
|
||||
y := oy
|
||||
canScrollPastBottom := gui.Config.GetUserConfig().Gui.ScrollPastBottom
|
||||
if !canScrollPastBottom {
|
||||
_, sy := view.Size()
|
||||
y += sy
|
||||
}
|
||||
scrollHeight := gui.Config.GetUserConfig().Gui.ScrollHeight
|
||||
scrollableLines := view.ViewLinesHeight() - y
|
||||
if scrollableLines < 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// margin is about how many lines must still appear if you scroll
|
||||
// all the way down. In practice every file ends in a newline so it will really
|
||||
// just show a single line
|
||||
margin := 1
|
||||
if canScrollPastBottom {
|
||||
margin = 2
|
||||
}
|
||||
if scrollableLines-margin < scrollHeight {
|
||||
scrollHeight = scrollableLines - margin
|
||||
}
|
||||
if oy+scrollHeight < 0 {
|
||||
return 0
|
||||
} else {
|
||||
return scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollUpMain() error {
|
||||
if gui.canScrollMergePanel() {
|
||||
gui.State.Panels.Merging.UserScrolling = true
|
||||
}
|
||||
|
||||
return gui.scrollUpView("main")
|
||||
return gui.scrollUpView(gui.Views.Main)
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollDownMain(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) scrollDownMain() error {
|
||||
if gui.canScrollMergePanel() {
|
||||
gui.State.Panels.Merging.UserScrolling = true
|
||||
}
|
||||
|
||||
return gui.scrollDownView("main")
|
||||
return gui.scrollDownView(gui.Views.Main)
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollUpSecondary(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.scrollUpView("secondary")
|
||||
func (gui *Gui) scrollUpSecondary() error {
|
||||
return gui.scrollUpView(gui.Views.Secondary)
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollDownSecondary(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.scrollDownView("secondary")
|
||||
func (gui *Gui) scrollDownSecondary() error {
|
||||
return gui.scrollDownView(gui.Views.Secondary)
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollUpConfirmationPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
if v.Editable {
|
||||
func (gui *Gui) scrollUpConfirmationPanel() error {
|
||||
if gui.Views.Confirmation.Editable {
|
||||
return nil
|
||||
}
|
||||
return gui.scrollUpView("confirmation")
|
||||
|
||||
return gui.scrollUpView(gui.Views.Confirmation)
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollDownConfirmationPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
if v.Editable {
|
||||
func (gui *Gui) scrollDownConfirmationPanel() error {
|
||||
if gui.Views.Confirmation.Editable {
|
||||
return nil
|
||||
}
|
||||
return gui.scrollDownView("confirmation")
|
||||
|
||||
return gui.scrollDownView(gui.Views.Confirmation)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRefresh(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleRefresh() error {
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMouseDownMain(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleMouseDownMain() error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch g.CurrentView().Name() {
|
||||
case "files":
|
||||
switch gui.currentSideContext() {
|
||||
case gui.State.Contexts.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 gui.enterFile(false, gui.Views.Main.SelectedLineIdx())
|
||||
case gui.State.Contexts.CommitFiles:
|
||||
return gui.enterCommitFile(gui.Views.Main.SelectedLineIdx())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMouseDownSecondary(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleMouseDownSecondary() error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch g.CurrentView().Name() {
|
||||
case "files":
|
||||
return gui.enterFile(true, v.SelectedLineIdx())
|
||||
switch gui.g.CurrentView() {
|
||||
case gui.Views.Files:
|
||||
return gui.enterFile(true, gui.Views.Secondary.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()
|
||||
|
||||
for _, mode := range gui.modeStatuses() {
|
||||
if mode.isActive() {
|
||||
if width-cx > len(gui.Tr.ResetInParentheses) {
|
||||
return nil
|
||||
}
|
||||
return mode.reset()
|
||||
}
|
||||
}
|
||||
|
||||
// if we're not in an active mode we show the donate button
|
||||
if cx <= len(gui.Tr.Donate)+len(INFO_SECTION_PADDING) {
|
||||
return gui.OSCommand.OpenLink("https://github.com/sponsors/jesseduffield")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) fetch(canPromptForCredentials bool) (err error) {
|
||||
func (gui *Gui) fetch(canPromptForCredentials bool, span string) (err error) {
|
||||
gui.Mutexes.FetchMutex.Lock()
|
||||
defer gui.Mutexes.FetchMutex.Unlock()
|
||||
|
||||
@@ -172,13 +196,13 @@ func (gui *Gui) fetch(canPromptForCredentials bool) (err error) {
|
||||
fetchOpts.PromptUserForCredential = gui.promptUserForCredential
|
||||
}
|
||||
|
||||
err = gui.GitCommand.Fetch(fetchOpts)
|
||||
err = gui.GitCommand.WithSpan(span).Fetch(fetchOpts)
|
||||
|
||||
if canPromptForCredentials && err != nil && strings.Contains(err.Error(), "exit status 128") {
|
||||
gui.createErrorPanel(gui.Tr.PassUnameWrong)
|
||||
_ = gui.createErrorPanel(gui.Tr.PassUnameWrong)
|
||||
}
|
||||
|
||||
gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, COMMITS, REMOTES, TAGS}, mode: ASYNC})
|
||||
_ = gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, COMMITS, REMOTES, TAGS}, mode: ASYNC})
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -191,5 +215,13 @@ func (gui *Gui) handleCopySelectedSideContextItemToClipboard() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.OSCommand.CopyToClipboard(itemId)
|
||||
if err := gui.OSCommand.WithSpan(gui.Tr.Spans.CopyToClipboard).CopyToClipboard(itemId); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
truncatedItemId := utils.TruncateWithEllipsis(strings.Replace(itemId, "\n", " ", -1), 50)
|
||||
|
||||
gui.raiseToast(fmt.Sprintf("'%s' %s", truncatedItemId, gui.Tr.LcCopiedToClipboard))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
41
pkg/gui/gpg.go
Normal file
41
pkg/gui/gpg.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package gui
|
||||
|
||||
// Currently there is a bug where if we switch to a subprocess from within
|
||||
// WithWaitingStatus we get stuck there and can't return to lazygit. We could
|
||||
// fix this bug, or just stop running subprocesses from within there, given that
|
||||
// we don't need to see a loading status if we're in a subprocess.
|
||||
func (gui *Gui) withGpgHandling(cmdStr string, waitingStatus string, onSuccess func() error) error {
|
||||
useSubprocess := gui.GitCommand.UsingGpg()
|
||||
if useSubprocess {
|
||||
// Need to remember why we use the shell for the subprocess but not in the other case
|
||||
// Maybe there's no good reason
|
||||
success, err := gui.runSubprocessWithSuspense(gui.OSCommand.ShellCommandFromString(cmdStr))
|
||||
if success && onSuccess != nil {
|
||||
if err := onSuccess(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return gui.WithWaitingStatus(waitingStatus, func() error {
|
||||
err := gui.OSCommand.RunCommand(cmdStr)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if onSuccess != nil {
|
||||
if err := onSuccess(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user