Compare commits
331 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27cd12e2d9 | ||
|
|
bfaf1c4f70 | ||
|
|
2d18d089ce | ||
|
|
9c7e40906d | ||
|
|
401f291c3b | ||
|
|
bea2ae5ff5 | ||
|
|
f49e4946f2 | ||
|
|
8ff74072f8 | ||
|
|
fcd5aea04e | ||
|
|
1c0da2967c | ||
|
|
1b78a42b80 | ||
|
|
79e73d2eff | ||
|
|
23299f88e9 | ||
|
|
ef744e45c1 | ||
|
|
660cc2f3d1 | ||
|
|
469ac116ef | ||
|
|
a86103479b | ||
|
|
d49e75bd3e | ||
|
|
f4718a9047 | ||
|
|
7d5fe4b66c | ||
|
|
845c80721f | ||
|
|
0e65db10d8 | ||
|
|
a9cc321981 | ||
|
|
6349214f00 | ||
|
|
96f821b841 | ||
|
|
964e3872c1 | ||
|
|
5dfa26ea8b | ||
|
|
dbf042b8ad | ||
|
|
014e06eefd | ||
|
|
39a2122dc0 | ||
|
|
fe6d8d62c5 | ||
|
|
570d27ffaa | ||
|
|
7b69aa1fda | ||
|
|
21e478dd59 | ||
|
|
d14fb36cb9 | ||
|
|
19a808642f | ||
|
|
e921ba0910 | ||
|
|
0f5a073d57 | ||
|
|
cb0bdd89c0 | ||
|
|
e89bf5d06b | ||
|
|
e82d2f37a1 | ||
|
|
65e955c622 | ||
|
|
e73f4c6b7e | ||
|
|
cf5cefb2d6 | ||
|
|
36ac764133 | ||
|
|
003e45d2f5 | ||
|
|
04e93317b8 | ||
|
|
f8dedb710b | ||
|
|
1c259f69f6 | ||
|
|
913f17ee3e | ||
|
|
6291c53966 | ||
|
|
267730bc00 | ||
|
|
d5db02a899 | ||
|
|
7ed8ee160d | ||
|
|
3dd33b65a0 | ||
|
|
b85048f616 | ||
|
|
0852f53455 | ||
|
|
10fa119ab3 | ||
|
|
b5404c6159 | ||
|
|
42d21c4bb6 | ||
|
|
cc13ae252a | ||
|
|
b97f844a3e | ||
|
|
1d6eb015c1 | ||
|
|
07a8ae8c3e | ||
|
|
f05a5e531e | ||
|
|
68586ec49a | ||
|
|
6cf75af0af | ||
|
|
304607ae5d | ||
|
|
e9f28855a2 | ||
|
|
66d7d5f312 | ||
|
|
59734f1069 | ||
|
|
2974a57943 | ||
|
|
fcdcd1c335 | ||
|
|
4a35f9fcdb | ||
|
|
674b14802e | ||
|
|
3e36affa69 | ||
|
|
97d7a8ad0c | ||
|
|
b89ba365d0 | ||
|
|
47ff388549 | ||
|
|
647ab9bf0f | ||
|
|
76431b4673 | ||
|
|
be0dd29e3a | ||
|
|
40fbce91ce | ||
|
|
33d287d2f0 | ||
|
|
9eb1cbc514 | ||
|
|
40b173118a | ||
|
|
8822c409e2 | ||
|
|
aa750c0819 | ||
|
|
d90d9d7330 | ||
|
|
a8db672ffb | ||
|
|
76b66ae26f | ||
|
|
a2790cfe8e | ||
|
|
624ae45ebb | ||
|
|
2756b82f57 | ||
|
|
52f41ab0d5 | ||
|
|
fbb767893e | ||
|
|
229f5ee48c | ||
|
|
96c7741ba0 | ||
|
|
517b7d0283 | ||
|
|
0c0231c3e8 | ||
|
|
a9559a5c87 | ||
|
|
814ee24c8d | ||
|
|
7876cddf4a | ||
|
|
e9051355a1 | ||
|
|
29316a528a | ||
|
|
036b53acf8 | ||
|
|
919463ff02 | ||
|
|
3f7ec3f3b8 | ||
|
|
19604214d7 | ||
|
|
f7add8d788 | ||
|
|
d97c230747 | ||
|
|
906a49049e | ||
|
|
c1a4bd0482 | ||
|
|
d0336fe16f | ||
|
|
61b4bbf74e | ||
|
|
384c2e13d7 | ||
|
|
198d237679 | ||
|
|
39315ca1e2 | ||
|
|
efb51eee96 | ||
|
|
fbbd16bd82 | ||
|
|
bd2c1eef53 | ||
|
|
d1395b15bb | ||
|
|
2d8ed5e274 | ||
|
|
6a5d8ba859 | ||
|
|
320e2a6536 | ||
|
|
3858118340 | ||
|
|
6420068569 | ||
|
|
95b147079f | ||
|
|
83757f1065 | ||
|
|
f2036b42e5 | ||
|
|
21b7d41845 | ||
|
|
91a404d033 | ||
|
|
d027cf969c | ||
|
|
c7f68a2ef9 | ||
|
|
78e55a05c1 | ||
|
|
ca71555d0b | ||
|
|
77fdac01ff | ||
|
|
8301fae01e | ||
|
|
e9161ad702 | ||
|
|
a0a139da1f | ||
|
|
8f13d1da91 | ||
|
|
d5fe9ce2c7 | ||
|
|
37acc17cf3 | ||
|
|
569ec5919c | ||
|
|
19719becf5 | ||
|
|
e64057b803 | ||
|
|
672667aa3e | ||
|
|
8a06b6067e | ||
|
|
2dcc52abd0 | ||
|
|
c831ad39c9 | ||
|
|
0cf78ea9ad | ||
|
|
3d51fbf354 | ||
|
|
e7a2c7cc3e | ||
|
|
708a078412 | ||
|
|
bbcc4b7b70 | ||
|
|
45bba0a3c5 | ||
|
|
d105e2690a | ||
|
|
32d3e497c3 | ||
|
|
30a5d1b486 | ||
|
|
6b3ea56add | ||
|
|
c3aefdb98e | ||
|
|
094939451d | ||
|
|
0e23f44b84 | ||
|
|
daecdd7c2b | ||
|
|
7c8df28d01 | ||
|
|
65917272a2 | ||
|
|
137fd80fdb | ||
|
|
98fbc61221 | ||
|
|
f80d15062b | ||
|
|
b1b0219f04 | ||
|
|
b1941c33f7 | ||
|
|
a15a7b607d | ||
|
|
d50283f5ee | ||
|
|
6508d3b872 | ||
|
|
65b8cef1b8 | ||
|
|
5d460e1e5e | ||
|
|
3d3e0be7bd | ||
|
|
c06c0b7133 | ||
|
|
91f6630907 | ||
|
|
60085cf679 | ||
|
|
389480b8fc | ||
|
|
b5c4f78e9d | ||
|
|
59b0e2d70a | ||
|
|
39bd1a4628 | ||
|
|
1c1445c896 | ||
|
|
1e8ade2431 | ||
|
|
a990fbc3eb | ||
|
|
e5574e7fe5 | ||
|
|
6c8a924fad | ||
|
|
64706257ca | ||
|
|
6183d92315 | ||
|
|
31823a7405 | ||
|
|
85ddd623f6 | ||
|
|
9212dda9c3 | ||
|
|
93d7b37c8d | ||
|
|
8470bcd71d | ||
|
|
3aab37611a | ||
|
|
8fbcc36331 | ||
|
|
dadb646252 | ||
|
|
0227b93409 | ||
|
|
b0ec0821d5 | ||
|
|
13a7806cac | ||
|
|
41c76fb748 | ||
|
|
ac0c3b9f92 | ||
|
|
1be0ff8da7 | ||
|
|
2169b5109f | ||
|
|
4a2292a53c | ||
|
|
7df4b736cf | ||
|
|
e47ad846c4 | ||
|
|
8f68ac2129 | ||
|
|
1ea2825a54 | ||
|
|
19146d61b1 | ||
|
|
e541b809ce | ||
|
|
6ca08c6519 | ||
|
|
b43540820b | ||
|
|
3d57da71eb | ||
|
|
0130fd3666 | ||
|
|
395afc4a8d | ||
|
|
31e201ca52 | ||
|
|
0abd7ad6be | ||
|
|
b3522c48d9 | ||
|
|
0fc58a7986 | ||
|
|
54241d8ab9 | ||
|
|
355f1615ab | ||
|
|
113252b0ae | ||
|
|
1cd7d14029 | ||
|
|
87c2fb6a4a | ||
|
|
9912998bb7 | ||
|
|
e223d3d8de | ||
|
|
ec31fc4cc7 | ||
|
|
3ce2b9b79a | ||
|
|
a79182e50d | ||
|
|
6f4c595dde | ||
|
|
0eb3090ad6 | ||
|
|
6ea25bd259 | ||
|
|
fe5f087f9c | ||
|
|
79299be3b2 | ||
|
|
4c9b620bd0 | ||
|
|
a7508a5dfd | ||
|
|
1a3d765c4c | ||
|
|
4058c71ca0 | ||
|
|
3fc22a6010 | ||
|
|
a9fe0b8000 | ||
|
|
5af7b0235e | ||
|
|
bf946200e9 | ||
|
|
890cc87724 | ||
|
|
8eb0b0f4ca | ||
|
|
e6a8dc0bcf | ||
|
|
02c497fad6 | ||
|
|
d0ab747479 | ||
|
|
f94d0be2c9 | ||
|
|
9fd9fd6816 | ||
|
|
b8717d750a | ||
|
|
8ad01fe32f | ||
|
|
fdb543fa7d | ||
|
|
52b5a6410c | ||
|
|
0034cfef5c | ||
|
|
78b62be96f | ||
|
|
1f5ccab1ce | ||
|
|
46be280c92 | ||
|
|
2a5763a771 | ||
|
|
370cec098b | ||
|
|
49a2f0191f | ||
|
|
fabdda0492 | ||
|
|
6fc3290a05 | ||
|
|
66e6369c28 | ||
|
|
0f0da9c32a | ||
|
|
0a69c1a02d | ||
|
|
feaf98bd01 | ||
|
|
0fe9c15ce8 | ||
|
|
f528e12c83 | ||
|
|
8ca9f93ccf | ||
|
|
73d8064837 | ||
|
|
5b1f60b7eb | ||
|
|
2e1344f611 | ||
|
|
5b9996b16f | ||
|
|
6fdc1791e4 | ||
|
|
fd4f37b5c3 | ||
|
|
d76e8887e5 | ||
|
|
eb9134685a | ||
|
|
d929b84786 | ||
|
|
8ef3297b11 | ||
|
|
27c7aeb117 | ||
|
|
c9714600e8 | ||
|
|
665fdded14 | ||
|
|
814a0ea36f | ||
|
|
71e018a3dd | ||
|
|
efb26f8b60 | ||
|
|
d9eb6e2682 | ||
|
|
b74107f2ba | ||
|
|
0cd91a10c6 | ||
|
|
f062e1dcda | ||
|
|
9f5397a2d4 | ||
|
|
0164abbd4a | ||
|
|
e92af63636 | ||
|
|
94501c683b | ||
|
|
047c3cf880 | ||
|
|
47d7d87c82 | ||
|
|
5f53d50492 | ||
|
|
5f71f87496 | ||
|
|
c6cb90e8ca | ||
|
|
fb156bcaac | ||
|
|
75ba2196ba | ||
|
|
4cb50b15e4 | ||
|
|
ca5cbe4d44 | ||
|
|
df050472a1 | ||
|
|
c173ebf5b9 | ||
|
|
434582b5f5 | ||
|
|
cf6be928a3 | ||
|
|
c907c55144 | ||
|
|
ee433ab909 | ||
|
|
bf69923b6d | ||
|
|
64782a433e | ||
|
|
44edb49a6e | ||
|
|
1a6d269063 | ||
|
|
b64953ebdb | ||
|
|
deaa2bcb15 | ||
|
|
c166c57c5d | ||
|
|
6b77e4ee4a | ||
|
|
e5534f060d | ||
|
|
466e0be560 | ||
|
|
810adab957 | ||
|
|
83a3c9fc8d | ||
|
|
5e95019b3f | ||
|
|
8e7f278094 | ||
|
|
83a895a463 | ||
|
|
59ae1e1599 | ||
|
|
77a82e9d51 | ||
|
|
bd79c2e8dc | ||
|
|
23bcc19180 | ||
|
|
282f08df36 |
@@ -1,62 +0,0 @@
|
||||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/golang:1.13
|
||||
environment:
|
||||
GO111MODULE: "on"
|
||||
working_directory: /go/src/github.com/jesseduffield/lazygit
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Run gofmt -s
|
||||
command: |
|
||||
if [ $(find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;|wc -l) -gt 0 ]; then
|
||||
find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;
|
||||
exit 1;
|
||||
fi
|
||||
- restore_cache:
|
||||
keys:
|
||||
- pkg-cache-{{ checksum "go.sum" }}-v5
|
||||
- run:
|
||||
name: Run tests
|
||||
command: |
|
||||
./test.sh
|
||||
- run:
|
||||
name: Push on codecov result
|
||||
command: |
|
||||
bash <(curl -s https://codecov.io/bash)
|
||||
- run:
|
||||
name: Compile project on every platform
|
||||
command: |
|
||||
go get github.com/mitchellh/gox
|
||||
GOFLAGS=-mod=vendor gox -parallel 10 -os "linux freebsd netbsd windows" -osarch "darwin/i386 darwin/amd64"
|
||||
- save_cache:
|
||||
key: pkg-cache-{{ checksum "go.sum" }}-v5
|
||||
paths:
|
||||
- ~/.cache/go-build
|
||||
|
||||
release:
|
||||
docker:
|
||||
- image: circleci/golang:1.13
|
||||
working_directory: /go/src/github.com/jesseduffield/lazygit
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Run gorelease
|
||||
command: |
|
||||
curl -sL https://git.io/goreleaser | bash
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build:
|
||||
jobs:
|
||||
- build
|
||||
release:
|
||||
jobs:
|
||||
- release:
|
||||
filters:
|
||||
tags:
|
||||
only: /v[0-9]+(\.[0-9]+)*/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1,5 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [jesseduffield]
|
||||
ko_fi: jesseduffield
|
||||
custom: ['https://donorbox.org/lazygit']
|
||||
|
||||
28
.github/workflows/automerge.yml
vendored
Normal file
28
.github/workflows/automerge.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: automerge
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
- unlabeled
|
||||
- synchronize
|
||||
- opened
|
||||
- edited
|
||||
- ready_for_review
|
||||
- reopened
|
||||
- unlocked
|
||||
pull_request_review:
|
||||
types:
|
||||
- submitted
|
||||
check_suite:
|
||||
types:
|
||||
- completed
|
||||
status: {}
|
||||
jobs:
|
||||
automerge:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: automerge
|
||||
uses: "pascalgn/automerge-action@135f0bdb927d9807b5446f7ca9ecc2c51de03c4a"
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
MERGE_METHOD: rebase
|
||||
28
.github/workflows/cd.yml
vendored
Normal file
28
.github/workflows/cd.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: Continuous Delivery
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
cd:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Unshallow repo
|
||||
run: git fetch --prune --unshallow
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.14.x
|
||||
- name: Run goreleaser
|
||||
uses: goreleaser/goreleaser-action@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_API_TOKEN}}
|
||||
- name: Bump Homebrew
|
||||
uses: dawidd6/action-homebrew-bump-formula@v3
|
||||
with:
|
||||
token: ${{secrets.GITHUB_API_TOKEN}}
|
||||
formula: lazygit
|
||||
40
.github/workflows/ci.yml
vendored
Normal file
40
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: Continuous Integration
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
pull_request: []
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GOFLAGS: -mod=vendor
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.14.x
|
||||
- name: Cache build
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cache/go-build
|
||||
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}
|
||||
restore-keys: |
|
||||
${{runner.os}}-go-
|
||||
- name: Format code
|
||||
run: |
|
||||
if [ $(find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;|wc -l) -gt 0 ]; then
|
||||
find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;
|
||||
exit 1
|
||||
fi
|
||||
- name: Test code
|
||||
run: |
|
||||
./test.sh
|
||||
- name: Build binaries
|
||||
uses: goreleaser/goreleaser-action@v1
|
||||
with:
|
||||
args: --skip-publish --snapshot
|
||||
@@ -38,27 +38,26 @@ changelog:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
- '^bump'
|
||||
brew:
|
||||
# Reporitory to push the tap to.
|
||||
github:
|
||||
owner: jesseduffield
|
||||
name: homebrew-lazygit
|
||||
brews:
|
||||
-
|
||||
# Repository to push the tap to.
|
||||
tap:
|
||||
owner: jesseduffield
|
||||
name: homebrew-lazygit
|
||||
|
||||
# Your app's homepage.
|
||||
# Default is empty.
|
||||
homepage: 'https://github.com/jesseduffield/lazygit/'
|
||||
# Your app's homepage.
|
||||
# Default is empty.
|
||||
homepage: 'https://github.com/jesseduffield/lazygit/'
|
||||
|
||||
# Your app's description.
|
||||
# Default is empty.
|
||||
description: 'A simple terminal UI for git commands, written in Go'
|
||||
# Your app's description.
|
||||
# Default is empty.
|
||||
description: 'A simple terminal UI for git commands, written in Go'
|
||||
|
||||
# # Packages your package depends on.
|
||||
# dependencies:
|
||||
# - git
|
||||
# - zsh
|
||||
# # Packages that conflict with your package.
|
||||
# conflicts:
|
||||
# - svn
|
||||
# - bash
|
||||
|
||||
# test comment to see if goreleaser only releases on new commits
|
||||
# # Packages your package depends on.
|
||||
# dependencies:
|
||||
# - git
|
||||
# - zsh
|
||||
# # Packages that conflict with your package.
|
||||
# conflicts:
|
||||
# - svn
|
||||
# - bash
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
# run with:
|
||||
# docker build -t lazygit .
|
||||
# docker run -it lazygit:latest /bin/sh -l
|
||||
# docker run -it lazygit:latest /bin/sh
|
||||
|
||||
FROM golang:1.13-alpine3.10
|
||||
FROM golang:1.14-alpine3.11
|
||||
WORKDIR /go/src/github.com/jesseduffield/lazygit/
|
||||
COPY ./ .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build
|
||||
|
||||
FROM alpine:3.10
|
||||
FROM alpine:3.11
|
||||
RUN apk add -U git xdg-utils
|
||||
WORKDIR /go/src/github.com/jesseduffield/lazygit/
|
||||
COPY --from=0 /go/src/github.com/jesseduffield/lazygit /go/src/github.com/jesseduffield/lazygit
|
||||
COPY --from=0 /go/src/github.com/jesseduffield/lazygit/lazygit /bin/
|
||||
RUN echo "alias gg=lazygit" >> ~/.profile
|
||||
|
||||
ENTRYPOINT [ "lazygit" ]
|
||||
|
||||
136
README.md
136
README.md
@@ -1,32 +1,58 @@
|
||||
# lazygit [](https://circleci.com/gh/jesseduffield/lazygit) [](https://codecov.io/gh/jesseduffield/lazygit) [](https://goreportcard.com/report/github.com/jesseduffield/lazygit) [](https://golangci.com) [](http://godoc.org/github.com/jesseduffield/lazygit) []()
|
||||
# lazygit
|
||||
|
||||
A simple terminal UI for git commands, written in Go with the [gocui](https://github.com/jroimartin/gocui 'gocui') library.
|
||||
 [](https://goreportcard.com/report/github.com/jesseduffield/lazygit) [](https://golangci.com) [](http://godoc.org/github.com/jesseduffield/lazygit) []() [](https://www.tickgit.com/browse?repo=github.com/jesseduffield/lazygit)
|
||||
|
||||
Rant time: You've heard it before, git is _powerful_, but what good is that power when everything is so damn hard to do? Interactive rebasing requires you to edit a goddamn TODO file in your editor? *Are you kidding me?* To stage part of a file you need to use a command line program stepping through each hunk and if a hunk can't be split down any further but contains code you don't want to stage, bad luck? *Are you KIDDING me?!* Sometimes you get asked to stash your changes when switching branches only to realise that after you switch and unstash that there weren't even any conflicts and it would have been fine to just checkout the branch directly? *YOU HAVE GOT TO BE KIDDING ME!*
|
||||
A simple terminal UI for git commands, written in Go with the [gocui](https://github.com/jroimartin/gocui "gocui") library.
|
||||
|
||||
Rant time: You've heard it before, git is _powerful_, but what good is that power when everything is so damn hard to do? Interactive rebasing requires you to edit a goddamn TODO file in your editor? *Are you kidding me?* To stage part of a file you need to use a command line program to step through each hunk and if a hunk can't be split down any further but contains code you don't want to stage, you have to edit an arcane patch file _by hand_? *Are you KIDDING me?!* Sometimes you get asked to stash your changes when switching branches only to realise that after you switch and unstash that there weren't even any conflicts and it would have been fine to just checkout the branch directly? *YOU HAVE GOT TO BE KIDDING ME!*
|
||||
|
||||
If you're a mere mortal like me and you're tired of hearing how powerful git is when in your daily life it's a powerful pain in your ass, lazygit might be for you.
|
||||
|
||||

|
||||

|
||||
|
||||
- [Installation](https://github.com/jesseduffield/lazygit#installation)
|
||||
- [Usage](https://github.com/jesseduffield/lazygit#usage),
|
||||
[Keybindings](/docs/keybindings)
|
||||
- [Cool Features](https://github.com/jesseduffield/lazygit#cool-features)
|
||||
- [Contributing](https://github.com/jesseduffield/lazygit#contributing)
|
||||
- [Video Tutorial](https://youtu.be/VDXvbHZYeKY)
|
||||
- [Rebase Magic Video Tutorial](https://youtu.be/4XaToVut_hs)
|
||||
- [Twitch Stream](https://www.twitch.tv/jesseduffield)
|
||||
## Table of contents
|
||||
|
||||
[<img src="https://i.imgur.com/sVEktDn.png">](https://youtu.be/CPLdltN7wgE)
|
||||
- [Installation](#installation)
|
||||
- [Binary releases](#binary-releases)
|
||||
- [Homebrew](#homebrew)
|
||||
- [MacPorts](#macports)
|
||||
- [Ubuntu](#ubuntu)
|
||||
- [Void Linux](#void-linux)
|
||||
- [Scoop (Windows)](#scoop-windows)
|
||||
- [Arch Linux](#arch-linux)
|
||||
- [Fedora and CentOS 7](#fedora-and-centos-7)
|
||||
- [Solus Linux](#solus-linux)
|
||||
- [FreeBSD](#freebsd)
|
||||
- [Conda](#conda)
|
||||
- [Go](#go)
|
||||
- [Usage](#usage)
|
||||
- [Keybindings](#keybindings)
|
||||
- [Changing directory on exit](#changing-directory-on-exit)
|
||||
- [Undo/Redo](#undoredo)
|
||||
- [Configuration](#configuration)
|
||||
- [Custom pagers](#configuration)
|
||||
- [Tutorials](#tutorials)
|
||||
- [Cool Features](#cool-features)
|
||||
- [Contributing](#contributing)
|
||||
- [Donate](#donate)
|
||||
- [Alternatives](#alternatives)
|
||||
|
||||
Github Sponsors is matching all donations dollar-for-dollar for 12 months so if you're feeling generous consider [sponsoring me](https://github.com/sponsors/jesseduffield)
|
||||
|
||||
[<img src="https://i.imgur.com/sVEktDn.png">](https://youtu.be/CPLdltN7wgE)
|
||||
|
||||
## Installation
|
||||
|
||||
### Binary Releases
|
||||
|
||||
For Windows, Mac OS or Linux, you can download a binary release [here](../../releases).
|
||||
|
||||
### Homebrew
|
||||
|
||||
Normally the lazygit formula can be found in the Homebrew core but we suggest you tap our formula to get the frequently updated one. It works with Linux, too.
|
||||
|
||||
Tap:
|
||||
|
||||
```
|
||||
brew install jesseduffield/lazygit/lazygit
|
||||
```
|
||||
@@ -38,8 +64,10 @@ brew install lazygit
|
||||
```
|
||||
|
||||
### MacPorts
|
||||
|
||||
Latest version built from github releases.
|
||||
Tap:
|
||||
|
||||
```
|
||||
sudo port install lazygit
|
||||
```
|
||||
@@ -64,6 +92,18 @@ They follow upstream latest releases
|
||||
sudo xbps-install -S lazygit
|
||||
```
|
||||
|
||||
### Scoop (Windows)
|
||||
|
||||
You can install `lazygit` using [scoop](https://scoop.sh/). It's in the `extras` bucket:
|
||||
|
||||
```sh
|
||||
# Add the extras bucket
|
||||
scoop bucket add extras
|
||||
|
||||
# Install lazygit
|
||||
scoop install lazygit
|
||||
```
|
||||
|
||||
### Arch Linux
|
||||
|
||||
Packages for Arch Linux are available via AUR (Arch User Repository).
|
||||
@@ -71,11 +111,11 @@ Packages for Arch Linux are available via AUR (Arch User Repository).
|
||||
There are two packages. The stable one which is built with the latest release
|
||||
and the git version which builds from the most recent commit.
|
||||
|
||||
- Stable: https://aur.archlinux.org/packages/lazygit/
|
||||
- Development: https://aur.archlinux.org/packages/lazygit-git/
|
||||
- Stable: <https://aur.archlinux.org/packages/lazygit/>
|
||||
- Development: <https://aur.archlinux.org/packages/lazygit-git/>
|
||||
|
||||
Instruction of how to install AUR content can be found here:
|
||||
https://wiki.archlinux.org/index.php/Arch_User_Repository
|
||||
<https://wiki.archlinux.org/index.php/Arch_User_Repository>
|
||||
|
||||
### Fedora and CentOS 7
|
||||
|
||||
@@ -86,18 +126,27 @@ sudo dnf copr enable atim/lazygit -y
|
||||
sudo dnf install lazygit
|
||||
```
|
||||
|
||||
### Solus Linux
|
||||
|
||||
```sh
|
||||
sudo eopkg install lazygit
|
||||
```
|
||||
|
||||
### FreeBSD
|
||||
|
||||
```sh
|
||||
pkg install lazygit
|
||||
```
|
||||
|
||||
|
||||
### Conda
|
||||
|
||||
Released versions are available for different platforms, see https://anaconda.org/conda-forge/lazygit
|
||||
Released versions are available for different platforms, see <https://anaconda.org/conda-forge/lazygit>
|
||||
|
||||
```sh
|
||||
conda install -c conda-forge lazygit
|
||||
```
|
||||
|
||||
### Binary Release (Windows/Linux/OSX)
|
||||
|
||||
You can download a binary release [here](https://github.com/jesseduffield/lazygit/releases).
|
||||
|
||||
### Go
|
||||
|
||||
```sh
|
||||
@@ -110,20 +159,27 @@ may need to add `~/go/bin` to your \$PATH (MacOS/Linux), or `%HOME%\go\bin`
|
||||
(Windows). Not to be mistaked for `C:\Go\bin` (which is for Go's own binaries,
|
||||
not apps like Lazygit).
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
Call `lazygit` in your terminal inside a git repository. If you want, you can
|
||||
Call `lazygit` in your terminal inside a git repository.
|
||||
|
||||
```sh
|
||||
$ lazygit
|
||||
```
|
||||
|
||||
If you want, you can
|
||||
also add an alias for this with `echo "alias lg='lazygit'" >> ~/.zshrc` (or
|
||||
whichever rc file you're using).
|
||||
|
||||
- Basic video tutorial [here](https://youtu.be/VDXvbHZYeKY).
|
||||
- Rebase Magic tutorial [here](https://youtu.be/4XaToVut_hs)
|
||||
- List of keybindings
|
||||
[here](/docs/keybindings).
|
||||
|
||||
## Changing Directory On Exit
|
||||
### Keybindings
|
||||
|
||||
You can check out the list of keybindings [here](/docs/keybindings).
|
||||
|
||||
### Changing Directory On Exit
|
||||
|
||||
If you change repos in lazygit and want your shell to change directory into that repo on exiting lazygit, add this to your `~/.zshrc` (or other rc file):
|
||||
|
||||
```
|
||||
lg()
|
||||
{
|
||||
@@ -137,8 +193,28 @@ lg()
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
Then `source ~/.zshrc` and from now on when you call `lg` and exit you'll switch directories to whatever you were in inside lazyigt. To override this behaviour you can exit using `shift+Q` rather than just `q`.
|
||||
|
||||
### Undo/Redo
|
||||
|
||||
See the [docs](/docs/Undoing.md)
|
||||
|
||||
## Configuration
|
||||
|
||||
Check out the [configuration docs](docs/Config.md).
|
||||
|
||||
### Custom Pagers
|
||||
|
||||
See the [docs](docs/Custom_Pagers.md)
|
||||
|
||||
## Tutorials
|
||||
|
||||
- [Video Tutorial](https://youtu.be/VDXvbHZYeKY)
|
||||
- [Rebase Magic Video Tutorial](https://youtu.be/4XaToVut_hs)
|
||||
- [Twitch Stream](https://www.twitch.tv/jesseduffield)
|
||||
|
||||
|
||||
## Cool features
|
||||
|
||||
- Adding files easily
|
||||
@@ -154,14 +230,14 @@ Then `source ~/.zshrc` and from now on when you call `lg` and exit you'll switch
|
||||
|
||||
### Interactive Rebasing
|
||||
|
||||

|
||||

|
||||
|
||||
## Contributing
|
||||
|
||||
We love your input! Please check out the [contributing guide](CONTRIBUTING.md).
|
||||
For contributor discussion about things not better discussed here in the repo, join the slack channel
|
||||
|
||||
[](https://join.slack.com/t/lazygit/shared_invite/enQtNDE3MjIwNTYyMDA0LTM3Yjk3NzdiYzhhNTA1YjM4Y2M4MWNmNDBkOTI0YTE4YjQ1ZmI2YWRhZTgwNjg2YzhhYjg3NDBlMmQyMTI5N2M)
|
||||
[](https://join.slack.com/t/lazygit/shared_invite/zt-5bo2clzo-hB8ZTVN5dWUCqj5QFiQVLA)
|
||||
|
||||
## Donate
|
||||
|
||||
|
||||
127
docs/Config.md
127
docs/Config.md
@@ -1,37 +1,59 @@
|
||||
# User Config:
|
||||
# User Config
|
||||
|
||||
Default path for the config file: `~/.config/jesseduffield/lazygit/config.yml`
|
||||
Default path for the config file:
|
||||
|
||||
## Default:
|
||||
* 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
|
||||
theme:
|
||||
lightTheme: false # For terminals with a light background
|
||||
activeBorderColor:
|
||||
- white
|
||||
- bold
|
||||
inactiveBorderColor:
|
||||
- white
|
||||
- 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
|
||||
update:
|
||||
method: prompt # can be: prompt | background | never
|
||||
days: 14 # how often an update is checked for
|
||||
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
|
||||
confirmOnQuit: false
|
||||
# determines whether hitting 'esc' will quit the application when there is nothing to cancel/close
|
||||
quitOnTopLevelReturn: true
|
||||
keybinding:
|
||||
universal:
|
||||
quit: 'q'
|
||||
@@ -43,10 +65,16 @@ Default path for the config file: `~/.config/jesseduffield/lazygit/config.yml`
|
||||
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>'
|
||||
@@ -61,7 +89,7 @@ Default path for the config file: `~/.config/jesseduffield/lazygit/config.yml`
|
||||
scrollDownMain-alt1: 'J' # main panel scrool down
|
||||
scrollUpMain-alt2: '<c-u>' # main panel scrool up
|
||||
scrollDownMain-alt2: '<c-d>' # main panel scrool down
|
||||
executeCustomCommand: 'X'
|
||||
executeCustomCommand: ':'
|
||||
createRebaseOptionsMenu: 'm'
|
||||
pushFiles: 'P'
|
||||
pullFiles: 'p'
|
||||
@@ -69,6 +97,13 @@ Default path for the config file: `~/.config/jesseduffield/lazygit/config.yml`
|
||||
createPatchOptionsMenu: '<c-p>'
|
||||
nextTab: ']'
|
||||
prevTab: '['
|
||||
nextScreenMode: '+'
|
||||
prevScreenMode: '_'
|
||||
undo: 'z'
|
||||
redo: '<c-z>'
|
||||
filteringMenu: '<c-s>'
|
||||
diffingMenu: '<c-e>'
|
||||
copyToClipboard: '<c-o>'
|
||||
status:
|
||||
checkForUpdate: 'u'
|
||||
recentRepos: '<enter>'
|
||||
@@ -112,8 +147,8 @@ Default path for the config file: `~/.config/jesseduffield/lazygit/config.yml`
|
||||
cherryPickCopyRange: 'C'
|
||||
pasteCommits: 'v'
|
||||
tagCommit: 'T'
|
||||
toggleDiffCommit: 'i'
|
||||
checkoutCommit: '<space>'
|
||||
resetCherryPick: '<c-R>'
|
||||
stash:
|
||||
popStash: 'g'
|
||||
commitFiles:
|
||||
@@ -123,33 +158,32 @@ Default path for the config file: `~/.config/jesseduffield/lazygit/config.yml`
|
||||
toggleDragSelect-alt: 'V'
|
||||
toggleSelectHunk: 'a'
|
||||
pickBothHunks: 'b'
|
||||
undo: 'z'
|
||||
```
|
||||
|
||||
## Platform Defaults:
|
||||
## Platform Defaults
|
||||
|
||||
### Windows:
|
||||
### Windows
|
||||
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'cmd /c "start "" {{filename}}"'
|
||||
```
|
||||
|
||||
### Linux:
|
||||
### Linux
|
||||
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'
|
||||
```
|
||||
|
||||
### OSX:
|
||||
### OSX
|
||||
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'open {{filename}}'
|
||||
```
|
||||
|
||||
### Recommended Config Values:
|
||||
### Recommended Config Values
|
||||
|
||||
for users of VSCode
|
||||
|
||||
@@ -158,7 +192,7 @@ for users of VSCode
|
||||
openCommand: 'code -r {{filename}}'
|
||||
```
|
||||
|
||||
## Color Attributes:
|
||||
## Color Attributes
|
||||
|
||||
For color attributes you can choose an array of attributes (with max one color attribute)
|
||||
The available attributes are:
|
||||
@@ -176,7 +210,7 @@ The available attributes are:
|
||||
- reverse # useful for high-contrast
|
||||
- underline
|
||||
|
||||
## Light terminal theme:
|
||||
## Light terminal theme
|
||||
|
||||
If you have issues with a light terminal theme where you can't read / see the text add these settings
|
||||
|
||||
@@ -189,17 +223,33 @@ If you have issues with a light terminal theme where you can't read / see the te
|
||||
- bold
|
||||
inactiveBorderColor:
|
||||
- black
|
||||
selectedLineBgColor:
|
||||
- default
|
||||
```
|
||||
|
||||
## Example Coloring:
|
||||
## Struggling to see selected line
|
||||
|
||||
If you struggle to see the selected line I recomment using the reverse attribute on selected lines like so:
|
||||
|
||||
```yaml
|
||||
gui:
|
||||
theme:
|
||||
selectedLineBgColor:
|
||||
- reverse
|
||||
selectedRangeBgColor:
|
||||
- reverse
|
||||
```
|
||||
|
||||
## Example Coloring
|
||||
|
||||

|
||||
|
||||
## Keybindings:
|
||||
For all possible keybinding options, check [Custom_Keybinding.md](https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybinding.md) <++>
|
||||
## Keybindings
|
||||
|
||||
For all possible keybinding options, check [Custom_Keybindings.md](https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md)
|
||||
|
||||
### Example Keybindings For Colemak Users
|
||||
|
||||
#### Example Keybindings For Colemak Users:
|
||||
```yaml
|
||||
keybinding:
|
||||
universal:
|
||||
@@ -207,6 +257,8 @@ For all possible keybinding options, check [Custom_Keybinding.md](https://github
|
||||
nextItem-alt: 'e'
|
||||
prevBlock-alt: 'n'
|
||||
nextBlock-alt: 'i'
|
||||
nextMatch: '='
|
||||
prevMatch: '-'
|
||||
new: 'k'
|
||||
edit: 'o'
|
||||
openFile: 'O'
|
||||
@@ -214,10 +266,49 @@ For all possible keybinding options, check [Custom_Keybinding.md](https://github
|
||||
scrollDownMain-alt1: 'E'
|
||||
scrollUpMain-alt2: '<c-u>'
|
||||
scrollDownMain-alt2: '<c-e>'
|
||||
undo: 'l'
|
||||
redo: '<c-r>'
|
||||
diffingMenu: 'M'
|
||||
filteringMenu: '<c-f>'
|
||||
files:
|
||||
ignoreFile: 'I'
|
||||
commits:
|
||||
moveDownCommit: '<c-e>'
|
||||
moveUpCommit: '<c-u>'
|
||||
branches:
|
||||
viewGitFlowOptions: 'I'
|
||||
setUpstream: 'U'
|
||||
```
|
||||
|
||||
## Custom pull request URLs
|
||||
|
||||
Some git provider setups (e.g. on-premises GitLab) can have distinct URLs for git-related calls and
|
||||
the web interface/API itself. To work with those, Lazygit needs to know where it needs to create
|
||||
the pull request. You can do so on your `config.yml` file using the following syntax:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
"<gitDomain>": "<provider>:<webDomain>"
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
- `gitDomain` stands for the domain used by git itself (i.e. the one present on clone URLs), e.g. `git.work.com`
|
||||
- `provider` is one of `github`, `bitbucket` or `gitlab`
|
||||
- `webDomain` is the URL where your git service exposes a web interface and APIs, e.g. `gitservice.work.com`
|
||||
|
||||
## Predefined commit message prefix
|
||||
In situations where certain naming pattern is used for branches and commits, pattern can be used to populate
|
||||
commit message with prefix that is parsed from the branch name.
|
||||
|
||||
Example:
|
||||
* Branch name: feature/AB-123
|
||||
* Commit message: [AB-123] Adding feature
|
||||
|
||||
```yaml
|
||||
git:
|
||||
commitPrefixes:
|
||||
my_project: # This is repository folder name
|
||||
pattern: "^\\w+\\/(\\w+-\\w+)"
|
||||
replace: "[$1] "
|
||||
```
|
||||
|
||||
64
docs/Custom_Pagers.md
Normal file
64
docs/Custom_Pagers.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Custom Pagers
|
||||
|
||||
Lazygit supports custom pagers, [configured](/docs/Config.md) in the config.yml file (which can be opened by pressing `o` in the Status panel).
|
||||
|
||||
Support does not extend to Windows users, because we're making use of a package which doesn't have Windows support.
|
||||
|
||||
## Default:
|
||||
|
||||
```yaml
|
||||
git:
|
||||
paging:
|
||||
colorArg: always
|
||||
useConfig: false
|
||||
```
|
||||
|
||||
the `colorArg` key is for whether you want the `--color=always` arg in your `git diff` command. Some pagers want it set to `always`, others want it set to `never`.
|
||||
|
||||
## Delta:
|
||||
|
||||
```yaml
|
||||
git:
|
||||
paging:
|
||||
colorArg: always
|
||||
pager: delta --dark --paging=never --24-bit-color=never
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Diff-so-fancy
|
||||
|
||||
```yaml
|
||||
git:
|
||||
paging:
|
||||
colorArg: always
|
||||
pager: diff-so-fancy
|
||||
```
|
||||
|
||||

|
||||
|
||||
## ydiff
|
||||
|
||||
```yaml
|
||||
gui:
|
||||
sidePanelWidth: 0.2 # gives you more space to show things side-by-side
|
||||
git:
|
||||
paging:
|
||||
colorArg: never
|
||||
pager: ydiff -p cat -s --wrap --width={{columnWidth}}
|
||||
```
|
||||
|
||||

|
||||
|
||||
Be careful with this one, I think the homebrew and pip versions are behind master. I needed to directly download the ydiff script to get the no-pager functionality working.
|
||||
|
||||
## Using git config
|
||||
|
||||
```yaml
|
||||
git:
|
||||
paging:
|
||||
colorArg: always
|
||||
useConfig: true
|
||||
```
|
||||
|
||||
If you set `useConfig: true`, lazygit will use whatever pager is specified in `$GIT_PAGER`, `$PAGER`, or your *git config*. If the pager ends with something like ` | less` we will strip that part out, because less doesn't play nice with our rendering approach. If the custom pager uses less under the hood, that will also break rendering (hence the `--paging=never` flag for the `delta` pager).
|
||||
24
docs/Undoing.md
Normal file
24
docs/Undoing.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Undo/Redo in lazygit
|
||||
|
||||

|
||||
|
||||
## Keybindings:
|
||||
'z' to undo, 'ctrl+z' to redo
|
||||
|
||||
## How it works
|
||||
|
||||
If you're as clumsy as me you'll probably have felt the pain of botching an interactive rebase or doing a hard reset onto the wrong commit. Luckily, the reflog allows you to trace your steps and make things right again, but I personally can't stand trying to make sense of the reflog.
|
||||
|
||||
Lazygit can read through your reflog for you and walk back action by action so that you don't even need to read the reflog. If lazygit finds a reflog entry where you checked out a branch, we'll checkout the original branch. If the entry is from a commit being applied, we'll go back to the commit before that. If we hit an interactive rebase, we'll go back to the commit you were on just before you started it.
|
||||
|
||||
## You can even undo things you did outside of lazygit!
|
||||
|
||||
Because lazygit just uses the reflog to keep track of things, it doesn't matter whether you're trying to undo something you did in lazygit or directly on the command line. You can open lazygit for the first time and start undoing thing in your repo! Likewise, lazygit marks its undos/redos in the reflog so if you quit the application and come back, lazygit still knows where you're up to.
|
||||
|
||||
## Limitations
|
||||
|
||||
There are limitations: firstly, lazygit can only undo things that are recorded in the reflog. That means changes to your working tree or stash aren't covered. Secondly, anything permanent you do like pushing to a remote can't be undone. Thirdly, actions like creating a branch won't be undone, because they're not stored in the reflog.
|
||||
|
||||
If you are mid-rebase, undo/redo is not supported, because the reflog doesn't enough contain information about what specific things have happened inside that rebase. If you want to undo out of a rebase, it's best to abort the rebase (the default keybinding for bringing up rebase options is 'm').
|
||||
|
||||
Undo/Redo is a new feature so if you find a bug let us know. The worst case scenario is that you'll just need to look at your reflog and manually put yourself back on track.
|
||||
@@ -1,47 +1,33 @@
|
||||
# Lazygit menu
|
||||
# Lazygit Keybindings
|
||||
|
||||
## Global
|
||||
## Global Keybindings
|
||||
|
||||
<pre>
|
||||
<kbd>pgup</kbd>: scroll up main panel (fn+up)
|
||||
<kbd>pgdown</kbd>: scroll down main panel (fn+down)
|
||||
<kbd>m</kbd>: view merge/rebase options
|
||||
<kbd>ctrl+p</kbd>: view custom patch options
|
||||
<kbd>P</kbd>: push
|
||||
<kbd>p</kbd>: pull
|
||||
<kbd>R</kbd>: refresh
|
||||
<kbd>x</kbd>: open menu
|
||||
<kbd>z</kbd>: undo (via reflog) (experimental)
|
||||
<kbd>ctrl+z</kbd>: redo (via reflog) (experimental)
|
||||
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: prev screen mode
|
||||
<kbd>:</kbd>: execute custom command
|
||||
<kbd>|</kbd>: view scoping options
|
||||
<kbd>ctrl+e</kbd>: open diff menu
|
||||
</pre>
|
||||
|
||||
## Status
|
||||
## Branches Panel
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: edit config file
|
||||
<kbd>o</kbd>: open config file
|
||||
<kbd>u</kbd>: check for update
|
||||
<kbd>s</kbd>: switch to a recent repo
|
||||
<kbd>]</kbd>: next tab
|
||||
<kbd>[</kbd>: previous tab
|
||||
</pre>
|
||||
|
||||
## Files
|
||||
|
||||
<pre>
|
||||
<kbd>c</kbd>: commit changes
|
||||
<kbd>w</kbd>: commit changes without pre-commit hook
|
||||
<kbd>A</kbd>: amend last commit
|
||||
<kbd>C</kbd>: commit changes using git editor
|
||||
<kbd>space</kbd>: toggle staged
|
||||
<kbd>d</kbd>: view 'discard changes' options
|
||||
<kbd>e</kbd>: edit file
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>i</kbd>: add to .gitignore
|
||||
<kbd>r</kbd>: refresh files
|
||||
<kbd>S</kbd>: stash files
|
||||
<kbd>a</kbd>: stage/unstage all
|
||||
<kbd>t</kbd>: add patch
|
||||
<kbd>D</kbd>: view reset options
|
||||
<kbd>enter</kbd>: stage individual hunks/lines
|
||||
<kbd>f</kbd>: fetch
|
||||
<kbd>X</kbd>: execute custom command
|
||||
</pre>
|
||||
|
||||
## Branches
|
||||
## Branches Panel (Branches Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: checkout
|
||||
@@ -50,12 +36,92 @@
|
||||
<kbd>F</kbd>: force checkout
|
||||
<kbd>n</kbd>: new branch
|
||||
<kbd>d</kbd>: delete branch
|
||||
<kbd>r</kbd>: rebase branch
|
||||
<kbd>r</kbd>: rebase checked-out branch onto this branch
|
||||
<kbd>M</kbd>: merge into currently checked out branch
|
||||
<kbd>i</kbd>: show git-flow options
|
||||
<kbd>f</kbd>: fast-forward this branch from its upstream
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>R</kbd>: rename branch
|
||||
<kbd>ctrl+o</kbd>: copy branch name to clipboard
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Commits
|
||||
## Branches Panel (Remote Branches (in Remotes tab))
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: return to remotes list
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>space</kbd>: checkout
|
||||
<kbd>n</kbd>: new branch
|
||||
<kbd>M</kbd>: merge into currently checked out branch
|
||||
<kbd>d</kbd>: delete branch
|
||||
<kbd>r</kbd>: rebase checked-out branch onto this branch
|
||||
<kbd>u</kbd>: set as upstream of checked-out branch
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Branches Panel (Remotes Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>f</kbd>: fetch remote
|
||||
<kbd>n</kbd>: add new remote
|
||||
<kbd>d</kbd>: remove remote
|
||||
<kbd>e</kbd>: edit remote
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Branches Panel (Tags Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: checkout
|
||||
<kbd>d</kbd>: delete tag
|
||||
<kbd>P</kbd>: push tag
|
||||
<kbd>n</kbd>: create tag
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Commit Files Panel
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: go back
|
||||
<kbd>c</kbd>: checkout file
|
||||
<kbd>d</kbd>: discard this commit's changes to this file
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>e</kbd>: edit file
|
||||
<kbd>space</kbd>: toggle file included in patch
|
||||
<kbd>enter</kbd>: enter file to add selected lines to the patch
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Commits Panel
|
||||
|
||||
<pre>
|
||||
<kbd>]</kbd>: next tab
|
||||
<kbd>[</kbd>: previous tab
|
||||
</pre>
|
||||
|
||||
## Commits Panel (Commits Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>s</kbd>: squash down
|
||||
@@ -73,49 +139,60 @@
|
||||
<kbd>p</kbd>: pick commit (when mid-rebase)
|
||||
<kbd>t</kbd>: revert commit
|
||||
<kbd>c</kbd>: copy commit (cherry-pick)
|
||||
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||
<kbd>C</kbd>: copy commit range (cherry-pick)
|
||||
<kbd>v</kbd>: paste commits (cherry-pick)
|
||||
<kbd>enter</kbd>: view commit's files
|
||||
<kbd>space</kbd>: select commit to diff with another commit
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>T</kbd>: tag commit
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Stash
|
||||
## Commits Panel (Reflog Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: apply
|
||||
<kbd>g</kbd>: pop
|
||||
<kbd>d</kbd>: drop
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Commit files
|
||||
## Files Panel
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: go back
|
||||
<kbd>c</kbd>: checkout file
|
||||
<kbd>d</kbd>: discard this commit's changes to this file
|
||||
<kbd>c</kbd>: commit changes
|
||||
<kbd>w</kbd>: commit changes without pre-commit hook
|
||||
<kbd>A</kbd>: amend last commit
|
||||
<kbd>C</kbd>: commit changes using git editor
|
||||
<kbd>space</kbd>: toggle staged
|
||||
<kbd>d</kbd>: view 'discard changes' options
|
||||
<kbd>e</kbd>: edit file
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>i</kbd>: add to .gitignore
|
||||
<kbd>r</kbd>: refresh files
|
||||
<kbd>s</kbd>: stash changes
|
||||
<kbd>S</kbd>: view stash options
|
||||
<kbd>a</kbd>: stage/unstage all
|
||||
<kbd>D</kbd>: view reset options
|
||||
<kbd>enter</kbd>: stage individual hunks/lines
|
||||
<kbd>f</kbd>: fetch
|
||||
<kbd>g</kbd>: view upstream reset options
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Main (Normal)
|
||||
|
||||
<pre>
|
||||
<kbd>PgDn</kbd>: scroll down (fn+up)
|
||||
<kbd>PgUp</kbd>: scroll up (fn+down)
|
||||
</pre>
|
||||
|
||||
## Main (Staging)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: return to files panel
|
||||
<kbd>▲</kbd>: select previous line
|
||||
<kbd>▼</kbd>: select next line
|
||||
<kbd>◄</kbd>: select previous hunk
|
||||
<kbd>►</kbd>: select next hunk
|
||||
<kbd>space</kbd>: stage line
|
||||
<kbd>a</kbd>: stage hunk
|
||||
</pre>
|
||||
|
||||
## Main (Merging)
|
||||
## Main Panel (Merging)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: return to files panel
|
||||
@@ -127,3 +204,78 @@
|
||||
<kbd>▼</kbd>: select bottom hunk
|
||||
<kbd>z</kbd>: undo
|
||||
</pre>
|
||||
|
||||
## Main Panel (Normal)
|
||||
|
||||
<pre>
|
||||
<kbd> ̄</kbd>: scroll down (fn+up)
|
||||
<kbd>¦</kbd>: scroll up (fn+down)
|
||||
</pre>
|
||||
|
||||
## Main Panel (Patch Building)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: exit line-by-line mode
|
||||
<kbd>▲</kbd>: select previous line
|
||||
<kbd>▼</kbd>: select next line
|
||||
<kbd>◄</kbd>: select previous hunk
|
||||
<kbd>►</kbd>: select next hunk
|
||||
<kbd>space</kbd>: add/remove line(s) to patch
|
||||
<kbd>v</kbd>: toggle drag select
|
||||
<kbd>V</kbd>: toggle drag select
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
</pre>
|
||||
|
||||
## Main Panel (Staging)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: return to files panel
|
||||
<kbd>space</kbd>: toggle line staged / unstaged
|
||||
<kbd>d</kbd>: delete change (git reset)
|
||||
<kbd>tab</kbd>: switch to other panel
|
||||
<kbd>▲</kbd>: select previous line
|
||||
<kbd>▼</kbd>: select next line
|
||||
<kbd>◄</kbd>: select previous hunk
|
||||
<kbd>►</kbd>: select next hunk
|
||||
<kbd>e</kbd>: edit file
|
||||
<kbd>o</kbd>: open file
|
||||
<kbd>v</kbd>: toggle drag select
|
||||
<kbd>V</kbd>: toggle drag select
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
<kbd>c</kbd>: commit changes
|
||||
<kbd>w</kbd>: commit changes without pre-commit hook
|
||||
<kbd>C</kbd>: commit changes using git editor
|
||||
</pre>
|
||||
|
||||
## Menu Panel
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: close menu
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Stash Panel
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: apply
|
||||
<kbd>g</kbd>: pop
|
||||
<kbd>d</kbd>: drop
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Status Panel
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: edit config file
|
||||
<kbd>o</kbd>: open config file
|
||||
<kbd>u</kbd>: check for update
|
||||
<kbd>enter</kbd>: switch to a recent repo
|
||||
</pre>
|
||||
|
||||
@@ -1,46 +1,33 @@
|
||||
# Lazygit menu
|
||||
# Lazygit Sneltoetsen
|
||||
|
||||
## Global
|
||||
|
||||
<pre>
|
||||
<kbd>pgup</kbd>: scroll omhoog naar hooft paneel (fn+up)
|
||||
<kbd>pgdown</kbd>: scroll beneden naar hooft paneel (fn+down)
|
||||
<kbd>m</kbd>: bekijk merge/rebase opties
|
||||
<kbd>ctrl+p</kbd>: bekijk aangepaste patch opties
|
||||
<kbd>P</kbd>: push
|
||||
<kbd>p</kbd>: pull
|
||||
<kbd>R</kbd>: verversen
|
||||
<kbd>x</kbd>: open menu
|
||||
<kbd>z</kbd>: ongedaan maken (via reflog) (experimenteel)
|
||||
<kbd>ctrl+z</kbd>: redo (via reflog) (experimenteel)
|
||||
<kbd>+</kbd>: volgende schermmode (normaal/half/groot )
|
||||
<kbd>_</kbd>: vorige schermmode
|
||||
<kbd>:</kbd>: voor aangepast commando uit
|
||||
<kbd>|</kbd>: view scoping options
|
||||
<kbd>ctrl+e</kbd>: open diff menu
|
||||
</pre>
|
||||
|
||||
## Status
|
||||
## Branches Paneel
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: verander config file
|
||||
<kbd>o</kbd>: open config file
|
||||
<kbd>u</kbd>: check voor updates
|
||||
<kbd>s</kbd>: wissel naar een recente repo
|
||||
<kbd>]</kbd>: volgende tab
|
||||
<kbd>[</kbd>: vorige tab
|
||||
</pre>
|
||||
|
||||
## Bestanden
|
||||
|
||||
<pre>
|
||||
<kbd>c</kbd>: Commit veranderingen
|
||||
<kbd>w</kbd>: commit veranderingen zonder pre-commit hook
|
||||
<kbd>A</kbd>: wijzig laatste commit
|
||||
<kbd>C</kbd>: commit veranderingen met de git editor
|
||||
<kbd>space</kbd>: toggle staged
|
||||
<kbd>d</kbd>: bekijk 'veranderingen ongedaan maken' opties
|
||||
<kbd>e</kbd>: verander bestand
|
||||
<kbd>o</kbd>: open bestand
|
||||
<kbd>i</kbd>: voeg toe aan .gitignore
|
||||
<kbd>r</kbd>: refresh bestanden
|
||||
<kbd>S</kbd>: stash-bestanden
|
||||
<kbd>a</kbd>: toggle staged alle
|
||||
<kbd>t</kbd>: bewerkingen toevoegen
|
||||
<kbd>D</kbd>: bekijk reset opties
|
||||
<kbd>enter</kbd>: stage individuele hunks/lijnen
|
||||
<kbd>f</kbd>: fetch
|
||||
<kbd>X</kbd>: voor aangepast commando uit
|
||||
</pre>
|
||||
|
||||
## Branches
|
||||
## Branches Paneel (Branches Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: uitchecken
|
||||
@@ -50,16 +37,96 @@
|
||||
<kbd>n</kbd>: nieuwe branch
|
||||
<kbd>d</kbd>: verwijder branch
|
||||
<kbd>r</kbd>: rebase branch
|
||||
<kbd>M</kbd>: merge in met huidige checked out branch
|
||||
<kbd>f</kbd>: fast-forward this branch from its upstream
|
||||
<kbd>M</kbd>: merge in met huidige uitgecheckte branch
|
||||
<kbd>i</kbd>: laat git-flow opties zien
|
||||
<kbd>f</kbd>: fast-forward deze branch van zijn upstream
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>R</kbd>: hernoem branch
|
||||
<kbd>ctrl+o</kbd>: copieer branch name clipboard
|
||||
<kbd>,</kbd>: vorige pagina
|
||||
<kbd>.</kbd>: volgende pagina
|
||||
<kbd><</kbd>: scroll naar bovenkant
|
||||
<kbd>/</kbd>: start met zoekken
|
||||
<kbd>></kbd>: scroll naar bodem
|
||||
</pre>
|
||||
|
||||
## Commits
|
||||
## Branches Paneel (Remote Branches (in Remotes tab))
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: ga terug naar remotes lijst
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>space</kbd>: uitchecken
|
||||
<kbd>n</kbd>: nieuwe branch
|
||||
<kbd>M</kbd>: merge in met huidige uitgecheckte branch
|
||||
<kbd>d</kbd>: verwijder branch
|
||||
<kbd>r</kbd>: rebase branch
|
||||
<kbd>u</kbd>: set as upstream of checked-out branch
|
||||
<kbd>,</kbd>: vorige pagina
|
||||
<kbd>.</kbd>: volgende pagina
|
||||
<kbd><</kbd>: scroll naar bovenkant
|
||||
<kbd>/</kbd>: start met zoekken
|
||||
<kbd>></kbd>: scroll naar bodem
|
||||
</pre>
|
||||
|
||||
## Branches Paneel (Remotes Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>f</kbd>: remote ophalen
|
||||
<kbd>n</kbd>: nieuwe remote toevoegen
|
||||
<kbd>d</kbd>: verwijder remote
|
||||
<kbd>e</kbd>: wijzig remote
|
||||
<kbd>,</kbd>: vorige pagina
|
||||
<kbd>.</kbd>: volgende pagina
|
||||
<kbd><</kbd>: scroll naar bovenkant
|
||||
<kbd>/</kbd>: start met zoekken
|
||||
<kbd>></kbd>: scroll naar bodem
|
||||
</pre>
|
||||
|
||||
## Branches Paneel (Tags Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: uitchecken
|
||||
<kbd>d</kbd>: verwijdert tag
|
||||
<kbd>P</kbd>: push tag
|
||||
<kbd>n</kbd>: nieuwe tag
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>,</kbd>: vorige pagina
|
||||
<kbd>.</kbd>: volgende pagina
|
||||
<kbd><</kbd>: scroll naar bovenkant
|
||||
<kbd>/</kbd>: start met zoekken
|
||||
<kbd>></kbd>: scroll naar bodem
|
||||
</pre>
|
||||
|
||||
## Commit bestanden Paneel
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: ga terug
|
||||
<kbd>c</kbd>: bestand uitchecken
|
||||
<kbd>d</kbd>: uitsluit deze commit zijn wijzigingen aan dit bestand
|
||||
<kbd>o</kbd>: open bestand
|
||||
<kbd>e</kbd>: verander bestand
|
||||
<kbd>space</kbd>: wissel bestand opgenomen in patch
|
||||
<kbd>enter</kbd>: open bestand om specifieke lijnen toe te voegen aan patch
|
||||
<kbd>,</kbd>: vorige pagina
|
||||
<kbd>.</kbd>: volgende pagina
|
||||
<kbd><</kbd>: scroll naar bovenkant
|
||||
<kbd>/</kbd>: start met zoekken
|
||||
<kbd>></kbd>: scroll naar bodem
|
||||
</pre>
|
||||
|
||||
## Commits Paneel
|
||||
|
||||
<pre>
|
||||
<kbd>]</kbd>: volgende tab
|
||||
<kbd>[</kbd>: vorige tab
|
||||
</pre>
|
||||
|
||||
## Commits Paneel (Commits Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>s</kbd>: squash beneden
|
||||
<kbd>r</kbd>: hernoem commit
|
||||
<kbd>R</kbd>: rename commit with editor
|
||||
<kbd>R</kbd>: hernoem commit met editor
|
||||
<kbd>g</kbd>: reset naar deze commit
|
||||
<kbd>f</kbd>: Fixup commit
|
||||
<kbd>F</kbd>: creëer fixup commit voor deze commit
|
||||
@@ -68,46 +135,64 @@
|
||||
<kbd>ctrl+j</kbd>: verplaats commit 1 omlaag
|
||||
<kbd>ctrl+k</kbd>: verplaats commit 1 omhoog
|
||||
<kbd>e</kbd>: verander commit
|
||||
<kbd>A</kbd>: wijzig commit met staged veranderingen
|
||||
<kbd>p</kbd>: pick commit (when mid-rebase)
|
||||
<kbd>A</kbd>: wijzig commit met staged wijzigingen
|
||||
<kbd>p</kbd>: pick commit (wanneer midden in rebase)
|
||||
<kbd>t</kbd>: commit omgedaan maken
|
||||
<kbd>c</kbd>: kopiëer commit (cherry-pick)
|
||||
<kbd>ctrl+o</kbd>: kopiëer commit SHA naar clipboard
|
||||
<kbd>C</kbd>: kopiëer commit reeks (cherry-pick)
|
||||
<kbd>v</kbd>: plak commits (cherry-pick)
|
||||
<kbd>enter</kbd>: bekijk gecommite bestanden
|
||||
<kbd>space</kbd>: select commit to diff with another commit
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>T</kbd>: tag commit
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (gecopieerde) commits selectie
|
||||
<kbd>,</kbd>: vorige pagina
|
||||
<kbd>.</kbd>: volgende pagina
|
||||
<kbd><</kbd>: scroll naar bovenkant
|
||||
<kbd>/</kbd>: start met zoekken
|
||||
<kbd>></kbd>: scroll naar bodem
|
||||
</pre>
|
||||
|
||||
## Stash
|
||||
## Commits Paneel (Reflog Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: toepassen
|
||||
<kbd>g</kbd>: pop
|
||||
<kbd>d</kbd>: drop
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>,</kbd>: vorige pagina
|
||||
<kbd>.</kbd>: volgende pagina
|
||||
<kbd><</kbd>: scroll naar bovenkant
|
||||
<kbd>/</kbd>: start met zoekken
|
||||
<kbd>></kbd>: scroll naar bodem
|
||||
</pre>
|
||||
|
||||
## Commit bestanden
|
||||
## Bestanden Paneel
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: ga terug
|
||||
<kbd>c</kbd>: bestand uitchecken
|
||||
<kbd>d</kbd>: uitsluit deze commit zijn veranderingen aan dit bestand
|
||||
<kbd>c</kbd>: commit wijzigingen
|
||||
<kbd>w</kbd>: commit wijzigingen zonder pre-commit hook
|
||||
<kbd>A</kbd>: wijzig laatste commit
|
||||
<kbd>C</kbd>: commit wijzigingen met de git editor
|
||||
<kbd>space</kbd>: wissel staged
|
||||
<kbd>d</kbd>: bekijk 'wijzigingen ongedaan maken' opties
|
||||
<kbd>e</kbd>: verander bestand
|
||||
<kbd>o</kbd>: open bestand
|
||||
<kbd>i</kbd>: voeg toe aan .gitignore
|
||||
<kbd>r</kbd>: bestanden vernieuwen
|
||||
<kbd>s</kbd>: stash-bestanden
|
||||
<kbd>S</kbd>: bekijk stash opties
|
||||
<kbd>a</kbd>: wissel staged alle
|
||||
<kbd>D</kbd>: bekijk reset opties
|
||||
<kbd>enter</kbd>: stage individuele hunks/lijnen
|
||||
<kbd>f</kbd>: fetch
|
||||
<kbd>g</kbd>: bekijk upstream reset optie
|
||||
<kbd>,</kbd>: vorige pagina
|
||||
<kbd>.</kbd>: volgende pagina
|
||||
<kbd><</kbd>: scroll naar bovenkant
|
||||
<kbd>/</kbd>: start met zoekken
|
||||
<kbd>></kbd>: scroll naar bodem
|
||||
</pre>
|
||||
|
||||
## Hoofd (Stage Lines/Hunks)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: ga terug naar het bestanden paneel
|
||||
<kbd>▲</kbd>: selecteer de vorige lijn
|
||||
<kbd>▼</kbd>: selecteer de volgende lijn
|
||||
<kbd>◄</kbd>: selecteer de vorige hunk
|
||||
<kbd>►</kbd>: selecteer de volgende hunk
|
||||
<kbd>space</kbd>: stage lijn
|
||||
<kbd>a</kbd>: stage hunk
|
||||
</pre>
|
||||
|
||||
## Hoofd (Merging)
|
||||
## Hoofd Paneel (Merging)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: ga terug naar het bestanden paneel
|
||||
@@ -120,9 +205,77 @@
|
||||
<kbd>z</kbd>: ongedaan maken
|
||||
</pre>
|
||||
|
||||
## Hoofd (Normaal)
|
||||
## Hoofd Paneel (Normaal)
|
||||
|
||||
<pre>
|
||||
<kbd>PgDn</kbd>: scroll omlaag (fn+up)
|
||||
<kbd>PgUp</kbd>: scroll omhoog (fn+down)
|
||||
<kbd> ̄</kbd>: scroll omlaag (fn+up)
|
||||
<kbd>¦</kbd>: scroll omhoog (fn+down)
|
||||
</pre>
|
||||
|
||||
## Hoofd Paneel (Patch Building)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: sluit lijn-bij-lijn mode
|
||||
<kbd>▲</kbd>: selecteer de vorige lijn
|
||||
<kbd>▼</kbd>: selecteer de volgende lijn
|
||||
<kbd>◄</kbd>: selecteer de vorige hunk
|
||||
<kbd>►</kbd>: selecteer de volgende hunk
|
||||
<kbd>space</kbd>: voeg toe/verwijdert lijn(en) in patch
|
||||
<kbd>v</kbd>: wissel drag selectie
|
||||
<kbd>V</kbd>: wissel drag selectie
|
||||
<kbd>a</kbd>: wissel selectie hunk
|
||||
</pre>
|
||||
|
||||
## Hoofd Paneel (Stage Lines/Hunks)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: ga terug naar het bestanden paneel
|
||||
<kbd>space</kbd>: wissel lijn staged / unstaged
|
||||
<kbd>d</kbd>: verwijdert wijziging (git reset)
|
||||
<kbd>tab</kbd>: ga naar ander paneel
|
||||
<kbd>▲</kbd>: selecteer de vorige lijn
|
||||
<kbd>▼</kbd>: selecteer de volgende lijn
|
||||
<kbd>◄</kbd>: selecteer de vorige hunk
|
||||
<kbd>►</kbd>: selecteer de volgende hunk
|
||||
<kbd>e</kbd>: verander bestand
|
||||
<kbd>o</kbd>: open bestand
|
||||
<kbd>v</kbd>: wissel drag selectie
|
||||
<kbd>V</kbd>: wissel drag selectie
|
||||
<kbd>a</kbd>: wissel selectie hunk
|
||||
<kbd>c</kbd>: commit wijzigingen
|
||||
<kbd>w</kbd>: commit wijzigingen zonder pre-commit hook
|
||||
<kbd>C</kbd>: commit wijzigingen met de git editor
|
||||
</pre>
|
||||
|
||||
## Menu Paneel
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: sluit menu
|
||||
<kbd>,</kbd>: vorige pagina
|
||||
<kbd>.</kbd>: volgende pagina
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Stash Paneel
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: toepassen
|
||||
<kbd>g</kbd>: pop
|
||||
<kbd>d</kbd>: drop
|
||||
<kbd>,</kbd>: vorige pagina
|
||||
<kbd>.</kbd>: volgende pagina
|
||||
<kbd><</kbd>: scroll naar bovenkant
|
||||
<kbd>/</kbd>: start met zoekken
|
||||
<kbd>></kbd>: scroll naar bodem
|
||||
</pre>
|
||||
|
||||
## Status Paneel
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: verander config file
|
||||
<kbd>o</kbd>: open config bestand
|
||||
<kbd>u</kbd>: check voor updates
|
||||
<kbd>enter</kbd>: wissel naar een recente repo
|
||||
</pre>
|
||||
|
||||
@@ -1,46 +1,33 @@
|
||||
# Lazygit menu
|
||||
# Lazygit Keybindings
|
||||
|
||||
## Globalne
|
||||
|
||||
<pre>
|
||||
<kbd>pgup</kbd>: scroll up main panel (fn+up)
|
||||
<kbd>pgdown</kbd>: scroll down main panel (fn+down)
|
||||
<kbd>m</kbd>: view merge/rebase options
|
||||
<kbd>ctrl+p</kbd>: view custom patch options
|
||||
<kbd>P</kbd>: push
|
||||
<kbd>p</kbd>: pull
|
||||
<kbd>R</kbd>: odśwież
|
||||
<kbd>x</kbd>: open menu
|
||||
<kbd>z</kbd>: undo (via reflog) (experimental)
|
||||
<kbd>ctrl+z</kbd>: redo (via reflog) (experimental)
|
||||
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: prev screen mode
|
||||
<kbd>:</kbd>: execute custom command
|
||||
<kbd>|</kbd>: view scoping options
|
||||
<kbd>ctrl+e</kbd>: open diff menu
|
||||
</pre>
|
||||
|
||||
## Status
|
||||
## Gałęzie Panel
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: edytuj plik konfiguracyjny
|
||||
<kbd>o</kbd>: otwórz plik konfiguracyjny
|
||||
<kbd>u</kbd>: sprawdź aktualizacje
|
||||
<kbd>s</kbd>: switch to a recent repo
|
||||
<kbd>]</kbd>: next tab
|
||||
<kbd>[</kbd>: previous tab
|
||||
</pre>
|
||||
|
||||
## Pliki
|
||||
|
||||
<pre>
|
||||
<kbd>c</kbd>: commituj zmiany
|
||||
<kbd>w</kbd>: commit changes without pre-commit hook
|
||||
<kbd>A</kbd>: zmień ostatnie zatwierdzenie
|
||||
<kbd>C</kbd>: commituj zmiany używając edytora z gita
|
||||
<kbd>space</kbd>: przełącz zatwierdzenie
|
||||
<kbd>d</kbd>: view 'discard changes' options
|
||||
<kbd>e</kbd>: edytuj plik
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>i</kbd>: dodaj do .gitignore
|
||||
<kbd>r</kbd>: odśwież pliki
|
||||
<kbd>S</kbd>: przechowaj pliki
|
||||
<kbd>a</kbd>: przełącz wszystkie zatwierdzenia
|
||||
<kbd>t</kbd>: dodaj łatkę
|
||||
<kbd>D</kbd>: view reset options
|
||||
<kbd>enter</kbd>: zatwierdź pojedyncze linie
|
||||
<kbd>f</kbd>: fetch
|
||||
<kbd>X</kbd>: execute custom command
|
||||
</pre>
|
||||
|
||||
## Gałęzie
|
||||
## Gałęzie Panel (Branches Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: przełącz
|
||||
@@ -51,10 +38,90 @@
|
||||
<kbd>d</kbd>: usuń gałąź
|
||||
<kbd>r</kbd>: rebase branch
|
||||
<kbd>M</kbd>: scal do obecnej gałęzi
|
||||
<kbd>i</kbd>: show git-flow options
|
||||
<kbd>f</kbd>: fast-forward this branch from its upstream
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>R</kbd>: rename branch
|
||||
<kbd>ctrl+o</kbd>: copy branch name to clipboard
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Commity
|
||||
## Gałęzie Panel (Remote Branches (in Remotes tab))
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: return to remotes list
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>space</kbd>: przełącz
|
||||
<kbd>n</kbd>: nowa gałąź
|
||||
<kbd>M</kbd>: scal do obecnej gałęzi
|
||||
<kbd>d</kbd>: usuń gałąź
|
||||
<kbd>r</kbd>: rebase branch
|
||||
<kbd>u</kbd>: set as upstream of checked-out branch
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Gałęzie Panel (Remotes Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>f</kbd>: fetch remote
|
||||
<kbd>n</kbd>: add new remote
|
||||
<kbd>d</kbd>: remove remote
|
||||
<kbd>e</kbd>: edit remote
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Gałęzie Panel (Tags Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: przełącz
|
||||
<kbd>d</kbd>: delete tag
|
||||
<kbd>P</kbd>: push tag
|
||||
<kbd>n</kbd>: create tag
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Commit files Panel
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: go back
|
||||
<kbd>c</kbd>: checkout file
|
||||
<kbd>d</kbd>: discard this commit's changes to this file
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>e</kbd>: edytuj plik
|
||||
<kbd>space</kbd>: toggle file included in patch
|
||||
<kbd>enter</kbd>: enter file to add selected lines to the patch
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Commity Panel
|
||||
|
||||
<pre>
|
||||
<kbd>]</kbd>: next tab
|
||||
<kbd>[</kbd>: previous tab
|
||||
</pre>
|
||||
|
||||
## Commity Panel (Commits Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>s</kbd>: ściśnij w dół
|
||||
@@ -72,49 +139,60 @@
|
||||
<kbd>p</kbd>: pick commit (when mid-rebase)
|
||||
<kbd>t</kbd>: revert commit
|
||||
<kbd>c</kbd>: copy commit (cherry-pick)
|
||||
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||
<kbd>C</kbd>: copy commit range (cherry-pick)
|
||||
<kbd>v</kbd>: paste commits (cherry-pick)
|
||||
<kbd>enter</kbd>: view commit's files
|
||||
<kbd>space</kbd>: select commit to diff with another commit
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>T</kbd>: tag commit
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Schowek
|
||||
## Commity Panel (Reflog Tab)
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: zastosuj
|
||||
<kbd>g</kbd>: wyciągnij
|
||||
<kbd>d</kbd>: porzuć
|
||||
<kbd>space</kbd>: checkout commit
|
||||
<kbd>g</kbd>: view reset options
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Commit files
|
||||
## Pliki Panel
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: go back
|
||||
<kbd>c</kbd>: checkout file
|
||||
<kbd>d</kbd>: discard this commit's changes to this file
|
||||
<kbd>c</kbd>: commituj zmiany
|
||||
<kbd>w</kbd>: commit changes without pre-commit hook
|
||||
<kbd>A</kbd>: zmień ostatnie zatwierdzenie
|
||||
<kbd>C</kbd>: commituj zmiany używając edytora z gita
|
||||
<kbd>space</kbd>: przełącz zatwierdzenie
|
||||
<kbd>d</kbd>: view 'discard changes' options
|
||||
<kbd>e</kbd>: edytuj plik
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>i</kbd>: dodaj do .gitignore
|
||||
<kbd>r</kbd>: odśwież pliki
|
||||
<kbd>s</kbd>: przechowaj pliki
|
||||
<kbd>S</kbd>: view stash options
|
||||
<kbd>a</kbd>: przełącz wszystkie zatwierdzenia
|
||||
<kbd>D</kbd>: view reset options
|
||||
<kbd>enter</kbd>: zatwierdź pojedyncze linie
|
||||
<kbd>f</kbd>: fetch
|
||||
<kbd>g</kbd>: view upstream reset options
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Main (Normal)
|
||||
|
||||
<pre>
|
||||
<kbd>PgDn</kbd>: scroll down (fn+up)
|
||||
<kbd>PgUp</kbd>: scroll up (fn+down)
|
||||
</pre>
|
||||
|
||||
## Main (Zatwierdzanie)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: wróć do panelu plików
|
||||
<kbd>▲</kbd>: select previous line
|
||||
<kbd>▼</kbd>: select next line
|
||||
<kbd>◄</kbd>: select previous hunk
|
||||
<kbd>►</kbd>: select next hunk
|
||||
<kbd>space</kbd>: zatwierdź linię
|
||||
<kbd>a</kbd>: zatwierdź kawałek
|
||||
</pre>
|
||||
|
||||
## Main (Merging)
|
||||
## Main Panel (Merging)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: wróć do panelu plików
|
||||
@@ -124,5 +202,80 @@
|
||||
<kbd>►</kbd>: select next conflict
|
||||
<kbd>▲</kbd>: select top hunk
|
||||
<kbd>▼</kbd>: select bottom hunk
|
||||
<kbd>z</kbd>: undo
|
||||
<kbd>z</kbd>: cofnij
|
||||
</pre>
|
||||
|
||||
## Main Panel (Normal)
|
||||
|
||||
<pre>
|
||||
<kbd> ̄</kbd>: scroll down (fn+up)
|
||||
<kbd>¦</kbd>: scroll up (fn+down)
|
||||
</pre>
|
||||
|
||||
## Main Panel (Patch Building)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: exit line-by-line mode
|
||||
<kbd>▲</kbd>: select previous line
|
||||
<kbd>▼</kbd>: select next line
|
||||
<kbd>◄</kbd>: select previous hunk
|
||||
<kbd>►</kbd>: select next hunk
|
||||
<kbd>space</kbd>: add/remove line(s) to patch
|
||||
<kbd>v</kbd>: toggle drag select
|
||||
<kbd>V</kbd>: toggle drag select
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
</pre>
|
||||
|
||||
## Main Panel (Zatwierdzanie)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: wróć do panelu plików
|
||||
<kbd>space</kbd>: toggle line staged / unstaged
|
||||
<kbd>d</kbd>: delete change (git reset)
|
||||
<kbd>tab</kbd>: switch to other panel
|
||||
<kbd>▲</kbd>: select previous line
|
||||
<kbd>▼</kbd>: select next line
|
||||
<kbd>◄</kbd>: select previous hunk
|
||||
<kbd>►</kbd>: select next hunk
|
||||
<kbd>e</kbd>: edytuj plik
|
||||
<kbd>o</kbd>: otwórz plik
|
||||
<kbd>v</kbd>: toggle drag select
|
||||
<kbd>V</kbd>: toggle drag select
|
||||
<kbd>a</kbd>: toggle select hunk
|
||||
<kbd>c</kbd>: commituj zmiany
|
||||
<kbd>w</kbd>: commit changes without pre-commit hook
|
||||
<kbd>C</kbd>: commituj zmiany używając edytora z gita
|
||||
</pre>
|
||||
|
||||
## Menu Panel
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: close menu
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Schowek Panel
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: zastosuj
|
||||
<kbd>g</kbd>: wyciągnij
|
||||
<kbd>d</kbd>: porzuć
|
||||
<kbd>,</kbd>: previous page
|
||||
<kbd>.</kbd>: next page
|
||||
<kbd><</kbd>: scroll to top
|
||||
<kbd>/</kbd>: start search
|
||||
<kbd>></kbd>: scroll to bottom
|
||||
</pre>
|
||||
|
||||
## Status Panel
|
||||
|
||||
<pre>
|
||||
<kbd>e</kbd>: edytuj plik konfiguracyjny
|
||||
<kbd>o</kbd>: otwórz plik konfiguracyjny
|
||||
<kbd>u</kbd>: sprawdź aktualizacje
|
||||
<kbd>enter</kbd>: switch to a recent repo
|
||||
</pre>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 178 KiB |
BIN
docs/resources/rebase.gif
Normal file
BIN
docs/resources/rebase.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
BIN
docs/resources/staging.gif
Normal file
BIN
docs/resources/staging.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 650 KiB |
BIN
docs/resources/undo2.gif
Normal file
BIN
docs/resources/undo2.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 539 KiB |
19
go.mod
19
go.mod
@@ -1,25 +1,25 @@
|
||||
module github.com/jesseduffield/lazygit
|
||||
|
||||
go 1.13
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
|
||||
github.com/creack/pty v1.1.10-0.20191209115840-8ab47f72e854
|
||||
github.com/fatih/color v1.7.0
|
||||
github.com/fsnotify/fsnotify v1.4.7
|
||||
github.com/go-errors/errors v1.0.1
|
||||
github.com/go-errors/errors v1.0.2
|
||||
github.com/go-git/go-git/v5 v5.0.0
|
||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
|
||||
github.com/golang/protobuf v1.3.2 // indirect
|
||||
github.com/google/go-cmp v0.3.1 // indirect
|
||||
github.com/integrii/flaggy v1.4.0
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20191116013947-b13bda319532
|
||||
github.com/jesseduffield/pty v1.2.1
|
||||
github.com/jesseduffield/rollrus v0.0.0-20190701125922-dd028cb0bfd7
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20190630083001-9dd53af7214e // indirect
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20200513110002-8cde0b9be542
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20200405031649-4dc645f7e8ba // indirect
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||
github.com/mattn/go-colorable v0.1.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.11 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.7
|
||||
github.com/mattn/go-runewidth v0.0.9
|
||||
github.com/mgutz/str v1.2.0
|
||||
github.com/nicksnyder/go-i18n/v2 v2.0.3
|
||||
github.com/onsi/ginkgo v1.10.3 // indirect
|
||||
@@ -34,11 +34,6 @@ require (
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
|
||||
github.com/stretchr/testify v1.4.0
|
||||
github.com/tcnksm/go-gitconfig v0.1.2
|
||||
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 // indirect
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect
|
||||
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 // indirect
|
||||
golang.org/x/text v0.3.2
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
gopkg.in/src-d/go-git.v4 v4.13.1
|
||||
gopkg.in/yaml.v2 v2.2.7
|
||||
)
|
||||
|
||||
88
go.sum
88
go.sum
@@ -23,7 +23,9 @@ github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.10-0.20191209115840-8ab47f72e854 h1:NB4neYMzyBsw52kUdkTrQm4Q05ErObCdwLvJptpfJSc=
|
||||
github.com/creack/pty v1.1.10-0.20191209115840-8ab47f72e854/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -42,6 +44,16 @@ github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
|
||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-errors/errors v1.0.2 h1:xMxH9j2fNg/L4hLn/4y3M0IUsn0M6Wbu/Uh9QlOfBh4=
|
||||
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
|
||||
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
|
||||
github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM=
|
||||
github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.0.1 h1:q+IFMfLx200Q3scvt2hN79JsEzy4AmBTp/pqnefH+Bc=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.0.1/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
|
||||
github.com/go-git/go-git/v5 v5.0.0 h1:k5RWPm4iJwYtfWoxIJy4wJX9ON7ihPeZZYC1fLYDnpg=
|
||||
github.com/go-git/go-git/v5 v5.0.0/go.mod h1:oYD8y9kWsGINPFJoLdaScGCN6dlKg23blmClfZwtUVA=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
@@ -54,7 +66,6 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
@@ -77,16 +88,14 @@ github.com/integrii/flaggy v1.4.0 h1:A1x7SYx4jqu5NSrY14z8Z+0UyX2S5ygfJJrfolWR3zM
|
||||
github.com/integrii/flaggy v1.4.0/go.mod h1:tnTxHeTJbah0gQ6/K0RW0J7fMUBk9MCF5blhm43LNpI=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20191116013947-b13bda319532 h1:V1Lk2rm5/p27NjnlF2ezzkxDaisHNcveMNueSD7RYgs=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20191116013947-b13bda319532/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/pty v1.2.1 h1:7xYBiwNH0PpWqC8JmvrPq1a/ksNqyCavzWu9pbBGYWI=
|
||||
github.com/jesseduffield/pty v1.2.1/go.mod h1:7jlS40+UhOqkZJDIG1B/H21xnuET/+fvbbnHCa8wSIo=
|
||||
github.com/jesseduffield/roll v0.0.0-20190629104057-695be2e62b00 h1:+JaOkfBNYQYlGD7dgru8mCwYNEc5tRRI8mThlVANhSM=
|
||||
github.com/jesseduffield/roll v0.0.0-20190629104057-695be2e62b00/go.mod h1:cWNQljQAWYBp4wchyGfql4q2jRNZXxiE1KhVQgz+JaM=
|
||||
github.com/jesseduffield/rollrus v0.0.0-20190701125922-dd028cb0bfd7 h1:CRD7bVjlGIiV+M0jlsa+XWpneW0KY0e7Y4z3GWb5S4o=
|
||||
github.com/jesseduffield/rollrus v0.0.0-20190701125922-dd028cb0bfd7/go.mod h1:VspA3aTkEo0Q7TPCLmX1uHNP+Wb4iSDX09hmTRo1QYc=
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20190630083001-9dd53af7214e h1:tth7wr6+sfSbdpRWWrwvLYyS56HyIRVfq0Qcl2h28wM=
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20190630083001-9dd53af7214e/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20200309001002-7765949e1c8a h1:JSORQue6V4bMppr22dtUuYX+w79cgupo66PcGZ9ijlU=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20200309001002-7765949e1c8a/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20200513110002-8cde0b9be542 h1:ezzJM/NZh5vgdHWupW4K6lWsnmVADzLqFa2E3zB2bzA=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20200513110002-8cde0b9be542/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20200130214842-1d31d1faa3c9 h1:iBBk1lhFwjwJw//J2m1yyz9S368GeXQTpMVACTyQMh0=
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20200130214842-1d31d1faa3c9/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4=
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20200405031649-4dc645f7e8ba h1:hWBdYchM9nZ2+GldroQQ627ISOC83iRDdpfwY+uyJeI=
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20200405031649-4dc645f7e8ba/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
@@ -98,7 +107,6 @@ github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v
|
||||
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
@@ -106,9 +114,10 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
@@ -116,8 +125,10 @@ github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
|
||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0=
|
||||
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mgutz/str v1.2.0 h1:4IzWSdIz9qPQWLfKZ0rJcV0jcUDpxvP4JVZ4GXQyvSw=
|
||||
github.com/mgutz/str v1.2.0/go.mod h1:w1v0ofgLaJdoD0HpQ3fycxKD1WtxpjSo151pK/31q6w=
|
||||
@@ -128,14 +139,14 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.0.3 h1:ks/JkQiOEhhuF6jpNvx+Wih1NIiXzUnZeZVnJuI8R8M=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.0.3/go.mod h1:oDab7q8XCYMRlcrBnaY/7B1eOectbvj6B1UPBT+p5jo=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY=
|
||||
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
|
||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
|
||||
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
|
||||
@@ -154,12 +165,11 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 h1:Xuk8ma/ibJ1fOy4Ee11vHhUFHQNpHhrBneOCNHVXS5w=
|
||||
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0/go.mod h1:7AwjWCpdPhkSmNAgUv5C7EJ4AbmjEB3r047r3DXWu3Y=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
@@ -168,17 +178,14 @@ github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIK
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
@@ -186,13 +193,9 @@ github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk=
|
||||
github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc=
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
|
||||
github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4=
|
||||
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||
@@ -213,10 +216,8 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf
|
||||
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g=
|
||||
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -227,11 +228,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -248,11 +246,9 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e h1:D5TXcfTk7xF7hvieo4QErS3qqCB4teTffacDWr7CI+0=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 h1:gSbV7h1NRL2G1xTg/owz62CST1oJBmxy4QpMMregXVQ=
|
||||
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/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=
|
||||
@@ -263,28 +259,22 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
|
||||
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
|
||||
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg=
|
||||
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
|
||||
gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE=
|
||||
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
|
||||
11
main.go
11
main.go
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
@@ -20,17 +19,15 @@ var (
|
||||
buildSource = "unknown"
|
||||
)
|
||||
|
||||
func projectPath(path string) string {
|
||||
gopath := os.Getenv("GOPATH")
|
||||
return filepath.FromSlash(gopath + "/src/github.com/jesseduffield/lazygit/" + path)
|
||||
}
|
||||
|
||||
func main() {
|
||||
flaggy.DefaultParser.ShowVersionWithVersionFlag = false
|
||||
|
||||
repoPath := "."
|
||||
flaggy.String(&repoPath, "p", "path", "Path of git repo")
|
||||
|
||||
filterPath := ""
|
||||
flaggy.String(&filterPath, "f", "filter", "Path to filter on in `git log -- <path>`. When in filter mode, the commits, reflog, and stash are filtered based on the given path, and some operations are restricted")
|
||||
|
||||
dump := ""
|
||||
flaggy.AddPositionalValue(&dump, "gitargs", 1, false, "Todo file")
|
||||
flaggy.DefaultParser.PositionalFlags[0].Hidden = true
|
||||
@@ -67,7 +64,7 @@ func main() {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
app, err := app.NewApp(appConfig)
|
||||
app, err := app.NewApp(appConfig, filterPath)
|
||||
|
||||
if err == nil {
|
||||
err = app.Run()
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/gui"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/updates"
|
||||
"github.com/jesseduffield/rollrus"
|
||||
"github.com/shibukawa/configdir"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -73,9 +72,7 @@ func newDevelopmentLogger(config config.AppConfigurer) *logrus.Logger {
|
||||
|
||||
func newLogger(config config.AppConfigurer) *logrus.Entry {
|
||||
var log *logrus.Logger
|
||||
environment := "production"
|
||||
if config.GetDebug() || os.Getenv("DEBUG") == "TRUE" {
|
||||
environment = "development"
|
||||
log = newDevelopmentLogger(config)
|
||||
} else {
|
||||
log = newProductionLogger(config)
|
||||
@@ -85,11 +82,6 @@ func newLogger(config config.AppConfigurer) *logrus.Entry {
|
||||
// https://github.com/aybabtme/humanlog
|
||||
log.Formatter = &logrus.JSONFormatter{}
|
||||
|
||||
if config.GetUserConfig().GetString("reporting") == "on" {
|
||||
// this isn't really a secret token: it only has permission to push new rollbar items
|
||||
hook := rollrus.NewHook("23432119147a4367abf7c0de2aa99a2d", environment)
|
||||
log.Hooks.Add(hook)
|
||||
}
|
||||
return log.WithFields(logrus.Fields{
|
||||
"debug": config.GetDebug(),
|
||||
"version": config.GetVersion(),
|
||||
@@ -99,7 +91,7 @@ func newLogger(config config.AppConfigurer) *logrus.Entry {
|
||||
}
|
||||
|
||||
// NewApp bootstrap a new application
|
||||
func NewApp(config config.AppConfigurer) (*App, error) {
|
||||
func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
|
||||
app := &App{
|
||||
closers: []io.Closer{},
|
||||
Config: config,
|
||||
@@ -129,7 +121,7 @@ func NewApp(config config.AppConfigurer) (*App, error) {
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config, app.Updater)
|
||||
app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config, app.Updater, filterPath)
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
@@ -139,9 +131,16 @@ func NewApp(config config.AppConfigurer) (*App, error) {
|
||||
func (app *App) setupRepo() error {
|
||||
// if we are not in a git repo, we ask if we want to `git init`
|
||||
if err := app.OSCommand.RunCommand("git status"); err != nil {
|
||||
if !strings.Contains(err.Error(), "Not a git repository") {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info, _ := os.Stat(filepath.Join(cwd, ".git"))
|
||||
if info != nil && info.IsDir() {
|
||||
return err // Current directory appears to be a git repository.
|
||||
}
|
||||
|
||||
// Offer to initialize a new repository in current directory.
|
||||
fmt.Print(app.Tr.SLocalize("CreateRepo"))
|
||||
response, _ := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||
if strings.Trim(response, " \n") != "y" {
|
||||
|
||||
@@ -1,47 +1,14 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// Branch : A git branch
|
||||
// duplicating this for now
|
||||
type Branch struct {
|
||||
Name string
|
||||
Recency string
|
||||
Pushables string
|
||||
Pullables string
|
||||
Selected bool
|
||||
}
|
||||
|
||||
// GetDisplayStrings returns the display string of branch
|
||||
func (b *Branch) GetDisplayStrings(isFocused bool) []string {
|
||||
displayName := utils.ColoredString(b.Name, GetBranchColor(b.Name))
|
||||
if isFocused && b.Selected && b.Pushables != "" && b.Pullables != "" {
|
||||
displayName = fmt.Sprintf("%s ↑%s↓%s", displayName, b.Pushables, b.Pullables)
|
||||
}
|
||||
|
||||
return []string{b.Recency, displayName}
|
||||
}
|
||||
|
||||
// GetBranchColor branch color
|
||||
func GetBranchColor(name string) color.Attribute {
|
||||
branchType := strings.Split(name, "/")[0]
|
||||
|
||||
switch branchType {
|
||||
case "feature":
|
||||
return color.FgGreen
|
||||
case "bugfix":
|
||||
return color.FgYellow
|
||||
case "hotfix":
|
||||
return color.FgRed
|
||||
default:
|
||||
return theme.DefaultTextColor
|
||||
}
|
||||
Name string
|
||||
// the displayname is something like '(HEAD detached at 123asdf)', whereas in that case the name would be '123asdf'
|
||||
DisplayName string
|
||||
Recency string
|
||||
Pushables string
|
||||
Pullables string
|
||||
UpstreamName string
|
||||
Head bool
|
||||
}
|
||||
|
||||
@@ -5,10 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"gopkg.in/src-d/go-git.v4/plumbing"
|
||||
)
|
||||
|
||||
// context:
|
||||
@@ -24,143 +21,140 @@ import (
|
||||
|
||||
// BranchListBuilder returns a list of Branch objects for the current repo
|
||||
type BranchListBuilder struct {
|
||||
Log *logrus.Entry
|
||||
GitCommand *GitCommand
|
||||
Log *logrus.Entry
|
||||
GitCommand *GitCommand
|
||||
ReflogCommits []*Commit
|
||||
}
|
||||
|
||||
// NewBranchListBuilder builds a new branch list builder
|
||||
func NewBranchListBuilder(log *logrus.Entry, gitCommand *GitCommand) (*BranchListBuilder, error) {
|
||||
func NewBranchListBuilder(log *logrus.Entry, gitCommand *GitCommand, reflogCommits []*Commit) (*BranchListBuilder, error) {
|
||||
return &BranchListBuilder{
|
||||
Log: log,
|
||||
GitCommand: gitCommand,
|
||||
Log: log,
|
||||
GitCommand: gitCommand,
|
||||
ReflogCommits: reflogCommits,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *BranchListBuilder) obtainCurrentBranch() *Branch {
|
||||
branchName, err := b.GitCommand.CurrentBranchName()
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
return &Branch{Name: strings.TrimSpace(branchName)}
|
||||
}
|
||||
|
||||
func (b *BranchListBuilder) obtainReflogBranches() []*Branch {
|
||||
branches := make([]*Branch, 0)
|
||||
// if we directly put this string in RunCommandWithOutput the compiler complains because it thinks it's a format string
|
||||
unescaped := "git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD"
|
||||
rawString, err := b.GitCommand.OSCommand.RunCommandWithOutput(unescaped)
|
||||
if err != nil {
|
||||
return branches
|
||||
}
|
||||
|
||||
branchLines := utils.SplitLines(rawString)
|
||||
for _, line := range branchLines {
|
||||
timeNumber, timeUnit, branchName := branchInfoFromLine(line)
|
||||
timeUnit = abbreviatedTimeUnit(timeUnit)
|
||||
branch := &Branch{Name: branchName, Recency: timeNumber + timeUnit}
|
||||
branches = append(branches, branch)
|
||||
}
|
||||
return uniqueByName(branches)
|
||||
}
|
||||
|
||||
func (b *BranchListBuilder) obtainSafeBranches() []*Branch {
|
||||
branches := make([]*Branch, 0)
|
||||
|
||||
bIter, err := b.GitCommand.Repo.Branches()
|
||||
func (b *BranchListBuilder) obtainBranches() []*Branch {
|
||||
cmdStr := `git for-each-ref --sort=-committerdate --format="%(HEAD)|%(refname:short)|%(upstream:short)|%(upstream:track)" refs/heads`
|
||||
output, err := b.GitCommand.OSCommand.RunCommandWithOutput(cmdStr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
bIter.ForEach(func(b *plumbing.Reference) error {
|
||||
name := b.Name().Short()
|
||||
branches = append(branches, &Branch{Name: name})
|
||||
return nil
|
||||
})
|
||||
|
||||
trimmedOutput := strings.TrimSpace(output)
|
||||
outputLines := strings.Split(trimmedOutput, "\n")
|
||||
branches := make([]*Branch, 0, len(outputLines))
|
||||
for _, line := range outputLines {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
split := strings.Split(line, SEPARATION_CHAR)
|
||||
|
||||
name := strings.TrimPrefix(split[1], "heads/")
|
||||
branch := &Branch{
|
||||
Name: name,
|
||||
Pullables: "?",
|
||||
Pushables: "?",
|
||||
Head: split[0] == "*",
|
||||
}
|
||||
|
||||
upstreamName := split[2]
|
||||
if upstreamName == "" {
|
||||
branches = append(branches, branch)
|
||||
continue
|
||||
}
|
||||
|
||||
branch.UpstreamName = upstreamName
|
||||
|
||||
track := split[3]
|
||||
re := regexp.MustCompile(`ahead (\d+)`)
|
||||
match := re.FindStringSubmatch(track)
|
||||
if len(match) > 1 {
|
||||
branch.Pushables = match[1]
|
||||
} else {
|
||||
branch.Pushables = "0"
|
||||
}
|
||||
|
||||
re = regexp.MustCompile(`behind (\d+)`)
|
||||
match = re.FindStringSubmatch(track)
|
||||
if len(match) > 1 {
|
||||
branch.Pullables = match[1]
|
||||
} else {
|
||||
branch.Pullables = "0"
|
||||
}
|
||||
|
||||
branches = append(branches, branch)
|
||||
}
|
||||
|
||||
return branches
|
||||
}
|
||||
|
||||
func (b *BranchListBuilder) appendNewBranches(finalBranches, newBranches, existingBranches []*Branch, included bool) []*Branch {
|
||||
for _, newBranch := range newBranches {
|
||||
if included == branchIncluded(newBranch.Name, existingBranches) {
|
||||
finalBranches = append(finalBranches, newBranch)
|
||||
}
|
||||
}
|
||||
return finalBranches
|
||||
}
|
||||
|
||||
func sanitisedReflogName(reflogBranch *Branch, safeBranches []*Branch) string {
|
||||
for _, safeBranch := range safeBranches {
|
||||
if strings.ToLower(safeBranch.Name) == strings.ToLower(reflogBranch.Name) {
|
||||
return safeBranch.Name
|
||||
}
|
||||
}
|
||||
return reflogBranch.Name
|
||||
}
|
||||
|
||||
// Build the list of branches for the current repo
|
||||
func (b *BranchListBuilder) Build() []*Branch {
|
||||
branches := make([]*Branch, 0)
|
||||
head := b.obtainCurrentBranch()
|
||||
safeBranches := b.obtainSafeBranches()
|
||||
branches := b.obtainBranches()
|
||||
|
||||
reflogBranches := b.obtainReflogBranches()
|
||||
for i, reflogBranch := range reflogBranches {
|
||||
reflogBranches[i].Name = sanitisedReflogName(reflogBranch, safeBranches)
|
||||
|
||||
// loop through reflog branches. If there is a match, merge them, then remove it from the branches and keep it in the reflog branches
|
||||
branchesWithRecency := make([]*Branch, 0)
|
||||
outer:
|
||||
for _, reflogBranch := range reflogBranches {
|
||||
for j, branch := range branches {
|
||||
if branch.Head {
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(reflogBranch.Name, branch.Name) {
|
||||
branch.Recency = reflogBranch.Recency
|
||||
branchesWithRecency = append(branchesWithRecency, branch)
|
||||
branches = append(branches[0:j], branches[j+1:]...)
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
branches = b.appendNewBranches(branches, reflogBranches, safeBranches, true)
|
||||
branches = b.appendNewBranches(branches, safeBranches, branches, false)
|
||||
branches = append(branchesWithRecency, branches...)
|
||||
|
||||
if len(branches) == 0 || branches[0].Name != head.Name {
|
||||
branches = append([]*Branch{head}, branches...)
|
||||
foundHead := false
|
||||
for i, branch := range branches {
|
||||
if branch.Head {
|
||||
foundHead = true
|
||||
branch.Recency = " *"
|
||||
branches = append(branches[0:i], branches[i+1:]...)
|
||||
branches = append([]*Branch{branch}, branches...)
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundHead {
|
||||
currentBranchName, currentBranchDisplayName, err := b.GitCommand.CurrentBranchName()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
branches = append([]*Branch{{Name: currentBranchName, DisplayName: currentBranchDisplayName, Head: true, Recency: " *"}}, branches...)
|
||||
}
|
||||
|
||||
branches[0].Recency = " *"
|
||||
|
||||
return branches
|
||||
}
|
||||
|
||||
func branchIncluded(branchName string, branches []*Branch) bool {
|
||||
for _, existingBranch := range branches {
|
||||
if strings.ToLower(existingBranch.Name) == strings.ToLower(branchName) {
|
||||
return true
|
||||
// TODO: only look at the new reflog commits, and otherwise store the recencies in
|
||||
// int form against the branch to recalculate the time ago
|
||||
func (b *BranchListBuilder) obtainReflogBranches() []*Branch {
|
||||
foundBranchesMap := map[string]bool{}
|
||||
re := regexp.MustCompile(`checkout: moving from ([\S]+) to ([\S]+)`)
|
||||
reflogBranches := make([]*Branch, 0, len(b.ReflogCommits))
|
||||
for _, commit := range b.ReflogCommits {
|
||||
if match := re.FindStringSubmatch(commit.Name); len(match) == 3 {
|
||||
recency := utils.UnixToTimeAgo(commit.UnixTimestamp)
|
||||
for _, branchName := range match[1:] {
|
||||
if !foundBranchesMap[branchName] {
|
||||
foundBranchesMap[branchName] = true
|
||||
reflogBranches = append(reflogBranches, &Branch{
|
||||
Recency: recency,
|
||||
Name: branchName,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func uniqueByName(branches []*Branch) []*Branch {
|
||||
finalBranches := make([]*Branch, 0)
|
||||
for _, branch := range branches {
|
||||
if branchIncluded(branch.Name, finalBranches) {
|
||||
continue
|
||||
}
|
||||
finalBranches = append(finalBranches, branch)
|
||||
}
|
||||
return finalBranches
|
||||
}
|
||||
|
||||
// A line will have the form '10 days ago master' so we need to strip out the
|
||||
// useful information from that into timeNumber, timeUnit, and branchName
|
||||
func branchInfoFromLine(line string) (string, string, string) {
|
||||
r := regexp.MustCompile("\\|.*\\s")
|
||||
line = r.ReplaceAllString(line, " ")
|
||||
words := strings.Split(line, " ")
|
||||
return words[0], words[1], words[len(words)-1]
|
||||
}
|
||||
|
||||
func abbreviatedTimeUnit(timeUnit string) string {
|
||||
r := regexp.MustCompile("s$")
|
||||
timeUnit = r.ReplaceAllString(timeUnit, "")
|
||||
timeUnitMap := map[string]string{
|
||||
"hour": "h",
|
||||
"minute": "m",
|
||||
"second": "s",
|
||||
"week": "w",
|
||||
"year": "y",
|
||||
"day": "d",
|
||||
"month": "m",
|
||||
}
|
||||
return timeUnitMap[timeUnit]
|
||||
return reflogBranches
|
||||
}
|
||||
|
||||
@@ -1,68 +1,20 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// Commit : A git commit
|
||||
type Commit struct {
|
||||
Sha string
|
||||
Name string
|
||||
Status string // one of "unpushed", "pushed", "merged", "rebasing" or "selected"
|
||||
DisplayString string
|
||||
Action string // one of "", "pick", "edit", "squash", "reword", "drop", "fixup"
|
||||
Copied bool // to know if this commit is ready to be cherry-picked somewhere
|
||||
Tags []string
|
||||
ExtraInfo string // something like 'HEAD -> master, tag: v0.15.2'
|
||||
Author string
|
||||
UnixTimestamp int64
|
||||
}
|
||||
|
||||
// GetDisplayStrings is a function.
|
||||
func (c *Commit) GetDisplayStrings(isFocused bool) []string {
|
||||
red := color.New(color.FgRed)
|
||||
yellow := color.New(color.FgYellow)
|
||||
green := color.New(color.FgGreen)
|
||||
blue := color.New(color.FgBlue)
|
||||
cyan := color.New(color.FgCyan)
|
||||
defaultColor := color.New(theme.DefaultTextColor)
|
||||
magenta := color.New(color.FgMagenta)
|
||||
|
||||
// for some reason, setting the background to blue pads out the other commits
|
||||
// horizontally. For the sake of accessibility I'm considering this a feature,
|
||||
// not a bug
|
||||
copied := color.New(color.FgCyan, color.BgBlue)
|
||||
|
||||
var shaColor *color.Color
|
||||
switch c.Status {
|
||||
case "unpushed":
|
||||
shaColor = red
|
||||
case "pushed":
|
||||
shaColor = yellow
|
||||
case "merged":
|
||||
shaColor = green
|
||||
case "rebasing":
|
||||
shaColor = blue
|
||||
case "reflog":
|
||||
shaColor = blue
|
||||
case "selected":
|
||||
shaColor = magenta
|
||||
default:
|
||||
shaColor = defaultColor
|
||||
func (c *Commit) ShortSha() string {
|
||||
if len(c.Sha) < 8 {
|
||||
return c.Sha
|
||||
}
|
||||
|
||||
if c.Copied {
|
||||
shaColor = copied
|
||||
}
|
||||
|
||||
actionString := ""
|
||||
tagString := ""
|
||||
if c.Action != "" {
|
||||
actionString = cyan.Sprint(utils.WithPadding(c.Action, 7)) + " "
|
||||
} else if len(c.Tags) > 0 {
|
||||
tagString = utils.ColoredString(strings.Join(c.Tags, " "), color.FgMagenta) + " "
|
||||
}
|
||||
|
||||
return []string{shaColor.Sprint(c.Sha), actionString + tagString + defaultColor.Sprint(c.Name)}
|
||||
return c.Sha[:8]
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
)
|
||||
|
||||
// CommitFile : A git commit file
|
||||
type CommitFile struct {
|
||||
Sha string
|
||||
@@ -22,21 +17,3 @@ const (
|
||||
// PART is for when you're only talking about specific lines that have been modified
|
||||
PART
|
||||
)
|
||||
|
||||
// GetDisplayStrings is a function.
|
||||
func (f *CommitFile) GetDisplayStrings(isFocused bool) []string {
|
||||
yellow := color.New(color.FgYellow)
|
||||
green := color.New(color.FgGreen)
|
||||
defaultColor := color.New(theme.DefaultTextColor)
|
||||
|
||||
var colour *color.Color
|
||||
switch f.Status {
|
||||
case UNSELECTED:
|
||||
colour = defaultColor
|
||||
case WHOLE:
|
||||
colour = green
|
||||
case PART:
|
||||
colour = yellow
|
||||
}
|
||||
return []string{colour.Sprint(f.DisplayString)}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
@@ -23,6 +25,8 @@ import (
|
||||
// if we find out we need to use one of these functions in the git.go file, we
|
||||
// can just pull them out of here and put them there and then call them from in here
|
||||
|
||||
const SEPARATION_CHAR = "|"
|
||||
|
||||
// CommitListBuilder returns a list of Branch objects for the current repo
|
||||
type CommitListBuilder struct {
|
||||
Log *logrus.Entry
|
||||
@@ -30,30 +34,67 @@ type CommitListBuilder struct {
|
||||
OSCommand *OSCommand
|
||||
Tr *i18n.Localizer
|
||||
CherryPickedCommits []*Commit
|
||||
DiffEntries []*Commit
|
||||
}
|
||||
|
||||
// NewCommitListBuilder builds a new commit list builder
|
||||
func NewCommitListBuilder(log *logrus.Entry, gitCommand *GitCommand, osCommand *OSCommand, tr *i18n.Localizer, cherryPickedCommits []*Commit, diffEntries []*Commit) (*CommitListBuilder, error) {
|
||||
func NewCommitListBuilder(log *logrus.Entry, gitCommand *GitCommand, osCommand *OSCommand, tr *i18n.Localizer, cherryPickedCommits []*Commit) (*CommitListBuilder, error) {
|
||||
return &CommitListBuilder{
|
||||
Log: log,
|
||||
GitCommand: gitCommand,
|
||||
OSCommand: osCommand,
|
||||
Tr: tr,
|
||||
CherryPickedCommits: cherryPickedCommits,
|
||||
DiffEntries: diffEntries,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// extractCommitFromLine takes a line from a git log and extracts the sha, message, date, and tag if present
|
||||
// then puts them into a commit object
|
||||
// example input:
|
||||
// 8ad01fe32fcc20f07bc6693f87aa4977c327f1e1|10 hours ago|Jesse Duffield| (HEAD -> master, tag: v0.15.2)|refresh commits when adding a tag
|
||||
func (c *CommitListBuilder) extractCommitFromLine(line string) *Commit {
|
||||
split := strings.Split(line, SEPARATION_CHAR)
|
||||
|
||||
sha := split[0]
|
||||
unixTimestamp := split[1]
|
||||
author := split[2]
|
||||
extraInfo := strings.TrimSpace(split[3])
|
||||
message := strings.Join(split[4:], SEPARATION_CHAR)
|
||||
tags := []string{}
|
||||
|
||||
if extraInfo != "" {
|
||||
re := regexp.MustCompile(`tag: ([^,\)]+)`)
|
||||
tagMatch := re.FindStringSubmatch(extraInfo)
|
||||
if len(tagMatch) > 1 {
|
||||
tags = append(tags, tagMatch[1])
|
||||
}
|
||||
}
|
||||
|
||||
unitTimestampInt, _ := strconv.Atoi(unixTimestamp)
|
||||
|
||||
return &Commit{
|
||||
Sha: sha,
|
||||
Name: message,
|
||||
Tags: tags,
|
||||
ExtraInfo: extraInfo,
|
||||
UnixTimestamp: int64(unitTimestampInt),
|
||||
Author: author,
|
||||
}
|
||||
}
|
||||
|
||||
type GetCommitsOptions struct {
|
||||
Limit bool
|
||||
FilterPath string
|
||||
}
|
||||
|
||||
// GetCommits obtains the commits of the current branch
|
||||
func (c *CommitListBuilder) GetCommits() ([]*Commit, error) {
|
||||
func (c *CommitListBuilder) GetCommits(options GetCommitsOptions) ([]*Commit, error) {
|
||||
commits := []*Commit{}
|
||||
var rebasingCommits []*Commit
|
||||
rebaseMode, err := c.GitCommand.RebaseMode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rebaseMode != "" {
|
||||
if rebaseMode != "" && options.FilterPath == "" {
|
||||
// here we want to also prepend the commits that we're in the process of rebasing
|
||||
rebasingCommits, err = c.getRebasingCommits(rebaseMode)
|
||||
if err != nil {
|
||||
@@ -65,22 +106,21 @@ func (c *CommitListBuilder) GetCommits() ([]*Commit, error) {
|
||||
}
|
||||
|
||||
unpushedCommits := c.getUnpushedCommits()
|
||||
log := c.getLog()
|
||||
cmd := c.getLogCmd(options)
|
||||
|
||||
// now we can split it up and turn it into commits
|
||||
for _, line := range utils.SplitLines(log) {
|
||||
splitLine := strings.Split(line, " ")
|
||||
sha := splitLine[0]
|
||||
_, unpushed := unpushedCommits[sha]
|
||||
status := map[bool]string{true: "unpushed", false: "pushed"}[unpushed]
|
||||
commits = append(commits, &Commit{
|
||||
Sha: sha,
|
||||
Name: strings.Join(splitLine[1:], " "),
|
||||
Status: status,
|
||||
DisplayString: strings.Join(splitLine, " "),
|
||||
// TODO: add tags here
|
||||
})
|
||||
err = RunLineOutputCmd(cmd, func(line string) (bool, error) {
|
||||
if strings.Split(line, " ")[0] != "gpg:" {
|
||||
commit := c.extractCommitFromLine(line)
|
||||
_, unpushed := unpushedCommits[commit.ShortSha()]
|
||||
commit.Status = map[bool]string{true: "unpushed", false: "pushed"}[unpushed]
|
||||
commits = append(commits, commit)
|
||||
}
|
||||
return false, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if rebaseMode != "" {
|
||||
currentCommit := commits[len(rebasingCommits)]
|
||||
blue := color.New(color.FgYellow)
|
||||
@@ -93,19 +133,6 @@ func (c *CommitListBuilder) GetCommits() ([]*Commit, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
commits, err = c.setCommitCherryPickStatuses(commits)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, commit := range commits {
|
||||
for _, entry := range c.DiffEntries {
|
||||
if entry.Sha == commit.Sha {
|
||||
commit.Status = "selected"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return commits, nil
|
||||
}
|
||||
|
||||
@@ -188,16 +215,19 @@ func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*Commit, error) {
|
||||
if line == "" || line == "noop" {
|
||||
return commits, nil
|
||||
}
|
||||
if strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
splitLine := strings.Split(line, " ")
|
||||
commits = append([]*Commit{{
|
||||
Sha: splitLine[1][0:7],
|
||||
Sha: splitLine[1],
|
||||
Name: strings.Join(splitLine[2:], " "),
|
||||
Status: "rebasing",
|
||||
Action: splitLine[0],
|
||||
}}, commits...)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
return commits, nil
|
||||
}
|
||||
|
||||
// assuming the file starts like this:
|
||||
@@ -207,7 +237,7 @@ func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*Commit, error) {
|
||||
// Subject: second commit on master
|
||||
func (c *CommitListBuilder) commitFromPatch(content string) (*Commit, error) {
|
||||
lines := strings.Split(content, "\n")
|
||||
sha := strings.Split(lines[0], " ")[1][0:7]
|
||||
sha := strings.Split(lines[0], " ")[1]
|
||||
name := strings.TrimPrefix(lines[3], "Subject: ")
|
||||
return &Commit{
|
||||
Sha: sha,
|
||||
@@ -239,19 +269,8 @@ func (c *CommitListBuilder) setCommitMergedStatuses(commits []*Commit) ([]*Commi
|
||||
return commits, nil
|
||||
}
|
||||
|
||||
func (c *CommitListBuilder) setCommitCherryPickStatuses(commits []*Commit) ([]*Commit, error) {
|
||||
for _, commit := range commits {
|
||||
for _, cherryPickedCommit := range c.CherryPickedCommits {
|
||||
if commit.Sha == cherryPickedCommit.Sha {
|
||||
commit.Copied = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return commits, nil
|
||||
}
|
||||
|
||||
func (c *CommitListBuilder) getMergeBase() (string, error) {
|
||||
currentBranch, err := c.GitCommand.CurrentBranchName()
|
||||
currentBranch, _, err := c.GitCommand.CurrentBranchName()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -270,7 +289,7 @@ func (c *CommitListBuilder) getMergeBase() (string, error) {
|
||||
// to the remote branch of the current branch, a map is returned to ease look up
|
||||
func (c *CommitListBuilder) getUnpushedCommits() map[string]bool {
|
||||
pushables := map[string]bool{}
|
||||
o, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..HEAD --abbrev-commit")
|
||||
o, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..HEAD --abbrev-commit --abbrev=8")
|
||||
if err != nil {
|
||||
return pushables
|
||||
}
|
||||
@@ -281,16 +300,17 @@ func (c *CommitListBuilder) getUnpushedCommits() map[string]bool {
|
||||
return pushables
|
||||
}
|
||||
|
||||
// getLog gets the git log (currently limited to 30 commits for performance
|
||||
// until we work out lazy loading
|
||||
func (c *CommitListBuilder) getLog() string {
|
||||
// currently limiting to 30 for performance reasons
|
||||
// TODO: add lazyloading when you scroll down
|
||||
result, err := c.OSCommand.RunCommandWithOutput("git log --oneline -30")
|
||||
if err != nil {
|
||||
// assume if there is an error there are no commits yet for this branch
|
||||
return ""
|
||||
// getLog gets the git log.
|
||||
func (c *CommitListBuilder) getLogCmd(options GetCommitsOptions) *exec.Cmd {
|
||||
limitFlag := ""
|
||||
if options.Limit {
|
||||
limitFlag = "-300"
|
||||
}
|
||||
|
||||
return result
|
||||
filterFlag := ""
|
||||
if options.FilterPath != "" {
|
||||
filterFlag = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(options.FilterPath))
|
||||
}
|
||||
|
||||
return c.OSCommand.ExecutableFromString(fmt.Sprintf("git log --oneline --pretty=format:\"%%H%s%%at%s%%aN%s%%d%s%%s\" %s --abbrev=%d --date=unix %s", SEPARATION_CHAR, SEPARATION_CHAR, SEPARATION_CHAR, SEPARATION_CHAR, limitFlag, 20, filterFlag))
|
||||
}
|
||||
|
||||
@@ -149,170 +149,3 @@ func TestCommitListBuilderGetMergeBase(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCommitListBuilderGetLog is a function.
|
||||
func TestCommitListBuilderGetLog(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(string)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Retrieves logs",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
|
||||
|
||||
return exec.Command("echo", "6f0b32f commands/git : add GetCommits tests refactor\n9d9d775 circle : remove new line")
|
||||
},
|
||||
func(output string) {
|
||||
assert.EqualValues(t, "6f0b32f commands/git : add GetCommits tests refactor\n9d9d775 circle : remove new line\n", output)
|
||||
},
|
||||
},
|
||||
{
|
||||
"An error occurred when retrieving logs",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
|
||||
return exec.Command("test")
|
||||
},
|
||||
func(output string) {
|
||||
assert.Empty(t, output)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
c := NewDummyCommitListBuilder()
|
||||
c.OSCommand.SetCommand(s.command)
|
||||
s.test(c.getLog())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCommitListBuilderGetCommits is a function.
|
||||
func TestCommitListBuilderGetCommits(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func([]*Commit, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"No data found",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
|
||||
switch args[0] {
|
||||
case "rev-list":
|
||||
assert.EqualValues(t, []string{"rev-list", "@{u}..HEAD", "--abbrev-commit"}, args)
|
||||
return exec.Command("echo")
|
||||
case "log":
|
||||
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
|
||||
return exec.Command("echo")
|
||||
case "merge-base":
|
||||
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
|
||||
return exec.Command("test")
|
||||
case "symbolic-ref":
|
||||
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
|
||||
return exec.Command("echo", "master")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
func(commits []*Commit, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, commits, 0)
|
||||
},
|
||||
},
|
||||
{
|
||||
"GetCommits returns 2 commits, 1 unpushed, the other merged",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
|
||||
switch args[0] {
|
||||
case "rev-list":
|
||||
assert.EqualValues(t, []string{"rev-list", "@{u}..HEAD", "--abbrev-commit"}, args)
|
||||
return exec.Command("echo", "8a2bb0e")
|
||||
case "log":
|
||||
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
|
||||
return exec.Command("echo", "8a2bb0e commit 1\n78976bc commit 2")
|
||||
case "merge-base":
|
||||
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
|
||||
return exec.Command("echo", "78976bc")
|
||||
case "symbolic-ref":
|
||||
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
|
||||
return exec.Command("echo", "master")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
func(commits []*Commit, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, commits, 2)
|
||||
assert.EqualValues(t, []*Commit{
|
||||
{
|
||||
Sha: "8a2bb0e",
|
||||
Name: "commit 1",
|
||||
Status: "unpushed",
|
||||
DisplayString: "8a2bb0e commit 1",
|
||||
},
|
||||
{
|
||||
Sha: "78976bc",
|
||||
Name: "commit 2",
|
||||
Status: "merged",
|
||||
DisplayString: "78976bc commit 2",
|
||||
},
|
||||
}, commits)
|
||||
},
|
||||
},
|
||||
{
|
||||
"GetCommits bubbles up an error from setCommitMergedStatuses",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
|
||||
switch args[0] {
|
||||
case "rev-list":
|
||||
assert.EqualValues(t, []string{"rev-list", "@{u}..HEAD", "--abbrev-commit"}, args)
|
||||
return exec.Command("echo", "8a2bb0e")
|
||||
case "log":
|
||||
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
|
||||
return exec.Command("echo", "8a2bb0e commit 1\n78976bc commit 2")
|
||||
case "merge-base":
|
||||
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
|
||||
return exec.Command("echo", "78976bc")
|
||||
case "symbolic-ref":
|
||||
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
|
||||
// here's where we are returning the error
|
||||
return exec.Command("test")
|
||||
case "branch":
|
||||
assert.EqualValues(t, []string{"branch", "--contains"}, args)
|
||||
// here too
|
||||
return exec.Command("test")
|
||||
case "rev-parse":
|
||||
assert.EqualValues(t, []string{"rev-parse", "--short", "HEAD"}, args)
|
||||
// here too
|
||||
return exec.Command("test")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
func(commits []*Commit, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.Len(t, commits, 0)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
c := NewDummyCommitListBuilder()
|
||||
c.OSCommand.SetCommand(s.command)
|
||||
s.test(c.GetCommits())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@ func NewDummyOSCommand() *OSCommand {
|
||||
|
||||
// NewDummyAppConfig creates a new dummy AppConfig for testing
|
||||
func NewDummyAppConfig() *config.AppConfig {
|
||||
userConfig := viper.New()
|
||||
userConfig.SetConfigType("yaml")
|
||||
if err := config.LoadDefaults(userConfig, config.GetDefaultConfig()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
appConfig := &config.AppConfig{
|
||||
Name: "lazygit",
|
||||
Version: "unversioned",
|
||||
@@ -26,7 +31,7 @@ func NewDummyAppConfig() *config.AppConfig {
|
||||
BuildDate: "",
|
||||
Debug: false,
|
||||
BuildSource: "",
|
||||
UserConfig: viper.New(),
|
||||
UserConfig: userConfig,
|
||||
}
|
||||
_ = yaml.Unmarshal([]byte{}, appConfig.AppState)
|
||||
return appConfig
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
"github.com/jesseduffield/pty"
|
||||
"github.com/creack/pty"
|
||||
)
|
||||
|
||||
// RunCommandWithOutputLiveWrapper runs a command and return every word that gets written in stdout
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package commands
|
||||
|
||||
import "github.com/fatih/color"
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// File : A file from git status
|
||||
// duplicating this for now
|
||||
@@ -17,22 +21,18 @@ type File struct {
|
||||
ShortStatus string // e.g. 'AD', ' A', 'M ', '??'
|
||||
}
|
||||
|
||||
// GetDisplayStrings returns the display string of a file
|
||||
func (f *File) GetDisplayStrings(isFocused bool) []string {
|
||||
// potentially inefficient to be instantiating these color
|
||||
// objects with each render
|
||||
red := color.New(color.FgRed)
|
||||
green := color.New(color.FgGreen)
|
||||
if !f.Tracked && !f.HasStagedChanges {
|
||||
return []string{red.Sprint(f.DisplayString)}
|
||||
}
|
||||
const RENAME_SEPARATOR = " -> "
|
||||
|
||||
output := green.Sprint(f.DisplayString[0:1])
|
||||
output += red.Sprint(f.DisplayString[1:3])
|
||||
if f.HasUnstagedChanges {
|
||||
output += red.Sprint(f.Name)
|
||||
} else {
|
||||
output += green.Sprint(f.Name)
|
||||
}
|
||||
return []string{output}
|
||||
func (f *File) IsRename() bool {
|
||||
return strings.Contains(f.Name, RENAME_SEPARATOR)
|
||||
}
|
||||
|
||||
// Names returns an array containing just the filename, or in the case of a rename, the after filename and the before filename
|
||||
func (f *File) Names() []string {
|
||||
return strings.Split(f.Name, RENAME_SEPARATOR)
|
||||
}
|
||||
|
||||
// returns true if the file names are the same or if a a file rename includes the filename of the other
|
||||
func (f *File) Matches(f2 *File) bool {
|
||||
return utils.StringArraysOverlap(f.Names(), f2.Names())
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -14,12 +15,12 @@ import (
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
gogit "github.com/go-git/go-git/v5"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
gitconfig "github.com/tcnksm/go-gitconfig"
|
||||
gogit "gopkg.in/src-d/go-git.v4"
|
||||
)
|
||||
|
||||
// this takes something like:
|
||||
@@ -84,6 +85,9 @@ type GitCommand struct {
|
||||
DotGitDir string
|
||||
onSuccessfulContinue func() error
|
||||
PatchManager *PatchManager
|
||||
|
||||
// Push to current determines whether the user has configured to push to the remote branch of the same name as the current or not
|
||||
PushToCurrent bool
|
||||
}
|
||||
|
||||
// NewGitCommand it runs git commands
|
||||
@@ -91,6 +95,15 @@ func NewGitCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localizer,
|
||||
var worktree *gogit.Worktree
|
||||
var repo *gogit.Repository
|
||||
|
||||
// see what our default push behaviour is
|
||||
output, err := osCommand.RunCommandWithOutput("git config --get push.default")
|
||||
pushToCurrent := false
|
||||
if err != nil {
|
||||
log.Errorf("error reading git config: %v", err)
|
||||
} else {
|
||||
pushToCurrent = strings.TrimSpace(output) == "current"
|
||||
}
|
||||
|
||||
fs := []func() error{
|
||||
func() error {
|
||||
return verifyInGitRepo(osCommand.RunCommand)
|
||||
@@ -127,6 +140,7 @@ func NewGitCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localizer,
|
||||
getLocalGitConfig: gitconfig.Local,
|
||||
removeFile: os.RemoveAll,
|
||||
DotGitDir: dotGitDir,
|
||||
PushToCurrent: pushToCurrent,
|
||||
}
|
||||
|
||||
gitCommand.PatchManager = NewPatchManager(log, gitCommand.ApplyPatch)
|
||||
@@ -155,9 +169,7 @@ func findDotGitDir(stat func(string) (os.FileInfo, error), readFile func(filenam
|
||||
return strings.TrimSpace(strings.TrimPrefix(fileContent, "gitdir: ")), nil
|
||||
}
|
||||
|
||||
// GetStashEntries stash entries
|
||||
func (c *GitCommand) GetStashEntries() []*StashEntry {
|
||||
// if we directly put this string in RunCommandWithOutput the compiler complains because it thinks it's a format string
|
||||
func (c *GitCommand) getUnfilteredStashEntries() []*StashEntry {
|
||||
unescaped := "git stash list --pretty='%gs'"
|
||||
rawString, _ := c.OSCommand.RunCommandWithOutput(unescaped)
|
||||
stashEntries := []*StashEntry{}
|
||||
@@ -167,22 +179,64 @@ func (c *GitCommand) GetStashEntries() []*StashEntry {
|
||||
return stashEntries
|
||||
}
|
||||
|
||||
// GetStashEntries stash entries
|
||||
func (c *GitCommand) GetStashEntries(filterPath string) []*StashEntry {
|
||||
if filterPath == "" {
|
||||
return c.getUnfilteredStashEntries()
|
||||
}
|
||||
|
||||
unescaped := fmt.Sprintf("git stash list --name-only")
|
||||
rawString, err := c.OSCommand.RunCommandWithOutput(unescaped)
|
||||
if err != nil {
|
||||
return c.getUnfilteredStashEntries()
|
||||
}
|
||||
stashEntries := []*StashEntry{}
|
||||
var currentStashEntry *StashEntry
|
||||
lines := utils.SplitLines(rawString)
|
||||
isAStash := func(line string) bool { return strings.HasPrefix(line, "stash@{") }
|
||||
re := regexp.MustCompile(`stash@\{(\d+)\}`)
|
||||
|
||||
outer:
|
||||
for i := 0; i < len(lines); i++ {
|
||||
if !isAStash(lines[i]) {
|
||||
continue
|
||||
}
|
||||
match := re.FindStringSubmatch(lines[i])
|
||||
idx, err := strconv.Atoi(match[1])
|
||||
if err != nil {
|
||||
return c.getUnfilteredStashEntries()
|
||||
}
|
||||
currentStashEntry = stashEntryFromLine(lines[i], idx)
|
||||
for i+1 < len(lines) && !isAStash(lines[i+1]) {
|
||||
i++
|
||||
if lines[i] == filterPath {
|
||||
stashEntries = append(stashEntries, currentStashEntry)
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
}
|
||||
return stashEntries
|
||||
}
|
||||
|
||||
func stashEntryFromLine(line string, index int) *StashEntry {
|
||||
return &StashEntry{
|
||||
Name: line,
|
||||
Index: index,
|
||||
DisplayString: line,
|
||||
Name: line,
|
||||
Index: index,
|
||||
}
|
||||
}
|
||||
|
||||
// GetStashEntryDiff stash diff
|
||||
func (c *GitCommand) GetStashEntryDiff(index int) (string, error) {
|
||||
return c.OSCommand.RunCommandWithOutput("git stash show -p --color stash@{%d}", index)
|
||||
func (c *GitCommand) ShowStashEntryCmdStr(index int) string {
|
||||
return fmt.Sprintf("git stash show -p --color=%s stash@{%d}", c.colorArg(), index)
|
||||
}
|
||||
|
||||
// GetStatusFiles git status files
|
||||
func (c *GitCommand) GetStatusFiles() []*File {
|
||||
statusOutput, _ := c.GitStatus()
|
||||
type GetStatusFileOptions struct {
|
||||
NoRenames bool
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*File {
|
||||
statusOutput, _ := c.GitStatus(GitStatusOptions{NoRenames: opts.NoRenames})
|
||||
statusStrings := utils.SplitLines(statusOutput)
|
||||
files := []*File{}
|
||||
|
||||
@@ -191,10 +245,10 @@ func (c *GitCommand) GetStatusFiles() []*File {
|
||||
stagedChange := change[0:1]
|
||||
unstagedChange := statusString[1:2]
|
||||
filename := c.OSCommand.Unquote(statusString[3:])
|
||||
_, untracked := map[string]bool{"??": true, "A ": true, "AM": true}[change]
|
||||
_, hasNoStagedChanges := map[string]bool{" ": true, "U": true, "?": true}[stagedChange]
|
||||
hasMergeConflicts := change == "UU" || change == "AA" || change == "DU"
|
||||
hasInlineMergeConflicts := change == "UU" || change == "AA"
|
||||
untracked := utils.IncludesString([]string{"??", "A ", "AM"}, change)
|
||||
hasNoStagedChanges := utils.IncludesString([]string{" ", "U", "?"}, stagedChange)
|
||||
hasMergeConflicts := utils.IncludesString([]string{"DD", "AA", "UU", "AU", "UA", "UD", "DU"}, change)
|
||||
hasInlineMergeConflicts := utils.IncludesString([]string{"UU", "AA"}, change)
|
||||
|
||||
file := &File{
|
||||
Name: filename,
|
||||
@@ -225,7 +279,7 @@ func (c *GitCommand) StashSave(message string) error {
|
||||
}
|
||||
|
||||
// MergeStatusFiles merge status files
|
||||
func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []*File) []*File {
|
||||
func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []*File, selectedFile *File) []*File {
|
||||
if len(oldFiles) == 0 {
|
||||
return newFiles
|
||||
}
|
||||
@@ -236,10 +290,15 @@ func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []*File) []*File {
|
||||
result := []*File{}
|
||||
for _, oldFile := range oldFiles {
|
||||
for newIndex, newFile := range newFiles {
|
||||
if oldFile.Name == newFile.Name {
|
||||
if includesInt(appendedIndexes, newIndex) {
|
||||
continue
|
||||
}
|
||||
// if we just staged B and in doing so created 'A -> B' and we are currently have oldFile: A and newFile: 'A -> B', we want to wait until we come across B so the our cursor isn't jumping anywhere
|
||||
waitForMatchingFile := selectedFile != nil && newFile.IsRename() && !selectedFile.IsRename() && newFile.Matches(selectedFile) && !oldFile.Matches(selectedFile)
|
||||
|
||||
if oldFile.Matches(newFile) && !waitForMatchingFile {
|
||||
result = append(result, newFile)
|
||||
appendedIndexes = append(appendedIndexes, newIndex)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -310,39 +369,64 @@ func (c *GitCommand) RebaseBranch(branchName string) error {
|
||||
return c.OSCommand.RunPreparedCommand(cmd)
|
||||
}
|
||||
|
||||
type FetchOptions struct {
|
||||
PromptUserForCredential func(string) string
|
||||
RemoteName string
|
||||
BranchName string
|
||||
}
|
||||
|
||||
// Fetch fetch git repo
|
||||
func (c *GitCommand) Fetch(unamePassQuestion func(string) string, canAskForCredentials bool) error {
|
||||
return c.OSCommand.DetectUnamePass("git fetch", func(question string) string {
|
||||
if canAskForCredentials {
|
||||
return unamePassQuestion(question)
|
||||
func (c *GitCommand) Fetch(opts FetchOptions) error {
|
||||
command := "git fetch"
|
||||
|
||||
if opts.RemoteName != "" {
|
||||
command = fmt.Sprintf("%s %s", command, opts.RemoteName)
|
||||
}
|
||||
if opts.BranchName != "" {
|
||||
command = fmt.Sprintf("%s %s", command, opts.BranchName)
|
||||
}
|
||||
|
||||
return c.OSCommand.DetectUnamePass(command, func(question string) string {
|
||||
if opts.PromptUserForCredential != nil {
|
||||
return opts.PromptUserForCredential(question)
|
||||
}
|
||||
return "\n"
|
||||
})
|
||||
}
|
||||
|
||||
// ResetToCommit reset to commit
|
||||
func (c *GitCommand) ResetToCommit(sha string, strength string) error {
|
||||
return c.OSCommand.RunCommand("git reset --%s %s", strength, sha)
|
||||
func (c *GitCommand) ResetToCommit(sha string, strength string, options RunCommandOptions) error {
|
||||
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git reset --%s %s", strength, sha), options)
|
||||
}
|
||||
|
||||
// NewBranch create new branch
|
||||
func (c *GitCommand) NewBranch(name string) error {
|
||||
return c.OSCommand.RunCommand("git checkout -b %s", name)
|
||||
func (c *GitCommand) NewBranch(name string, baseBranch string) error {
|
||||
return c.OSCommand.RunCommand("git checkout -b %s %s", name, baseBranch)
|
||||
}
|
||||
|
||||
// CurrentBranchName is a function.
|
||||
func (c *GitCommand) CurrentBranchName() (string, error) {
|
||||
// CurrentBranchName get the current branch name and displayname.
|
||||
// the first returned string is the name and the second is the displayname
|
||||
// e.g. name is 123asdf and displayname is '(HEAD detached at 123asdf)'
|
||||
func (c *GitCommand) CurrentBranchName() (string, string, error) {
|
||||
branchName, err := c.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD")
|
||||
if err != nil || branchName == "HEAD\n" {
|
||||
output, err := c.OSCommand.RunCommandWithOutput("git branch --contains")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
re := regexp.MustCompile(CurrentBranchNameRegex)
|
||||
match := re.FindStringSubmatch(output)
|
||||
branchName = match[1]
|
||||
if err == nil && branchName != "HEAD\n" {
|
||||
trimmedBranchName := strings.TrimSpace(branchName)
|
||||
return trimmedBranchName, trimmedBranchName, nil
|
||||
}
|
||||
return utils.TrimTrailingNewline(branchName), nil
|
||||
output, err := c.OSCommand.RunCommandWithOutput("git branch --contains")
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
for _, line := range utils.SplitLines(output) {
|
||||
re := regexp.MustCompile(CurrentBranchNameRegex)
|
||||
match := re.FindStringSubmatch(line)
|
||||
if len(match) > 0 {
|
||||
branchName = match[1]
|
||||
displayBranchName := match[0][2:]
|
||||
return branchName, displayBranchName, nil
|
||||
}
|
||||
}
|
||||
return "HEAD", "HEAD", nil
|
||||
}
|
||||
|
||||
// DeleteBranch delete branch
|
||||
@@ -361,9 +445,20 @@ func (c *GitCommand) ListStash() (string, error) {
|
||||
return c.OSCommand.RunCommandWithOutput("git stash list")
|
||||
}
|
||||
|
||||
type MergeOpts struct {
|
||||
FastForwardOnly bool
|
||||
}
|
||||
|
||||
// Merge merge
|
||||
func (c *GitCommand) Merge(branchName string) error {
|
||||
return c.OSCommand.RunCommand("git merge --no-edit %s", branchName)
|
||||
func (c *GitCommand) Merge(branchName string, opts MergeOpts) error {
|
||||
mergeArgs := c.Config.GetUserConfig().GetString("git.merging.args")
|
||||
|
||||
command := fmt.Sprintf("git merge --no-edit %s %s", mergeArgs, branchName)
|
||||
if opts.FastForwardOnly {
|
||||
command = fmt.Sprintf("%s --ff-only", command)
|
||||
}
|
||||
|
||||
return c.OSCommand.RunCommand(command)
|
||||
}
|
||||
|
||||
// AbortMerge abort merge
|
||||
@@ -374,6 +469,11 @@ func (c *GitCommand) AbortMerge() error {
|
||||
// usingGpg tells us whether the user has gpg enabled so that we can know
|
||||
// whether we need to run a subprocess to allow them to enter their password
|
||||
func (c *GitCommand) usingGpg() bool {
|
||||
overrideGpg := c.Config.GetUserConfig().GetBool("git.overrideGpg")
|
||||
if overrideGpg {
|
||||
return false
|
||||
}
|
||||
|
||||
gpgsign, _ := c.getLocalGitConfig("commit.gpgsign")
|
||||
if gpgsign == "" {
|
||||
gpgsign, _ = c.getGlobalGitConfig("commit.gpgsign")
|
||||
@@ -385,31 +485,33 @@ func (c *GitCommand) usingGpg() bool {
|
||||
|
||||
// Commit commits to git
|
||||
func (c *GitCommand) Commit(message string, flags string) (*exec.Cmd, error) {
|
||||
command := fmt.Sprintf("git commit %s -m %s", flags, c.OSCommand.Quote(message))
|
||||
command := fmt.Sprintf("git commit %s -m %s", flags, strconv.Quote(message))
|
||||
if c.usingGpg() {
|
||||
return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command), nil
|
||||
return c.OSCommand.ShellCommandFromString(command), nil
|
||||
}
|
||||
|
||||
return nil, c.OSCommand.RunCommand(command)
|
||||
}
|
||||
|
||||
// Get the subject of the HEAD commit
|
||||
func (c *GitCommand) GetHeadCommitMessage() (string, error) {
|
||||
cmdStr := "git log -1 --pretty=%s"
|
||||
message, err := c.OSCommand.RunCommandWithOutput(cmdStr)
|
||||
return strings.TrimSpace(message), err
|
||||
}
|
||||
|
||||
// AmendHead amends HEAD with whatever is staged in your working tree
|
||||
func (c *GitCommand) AmendHead() (*exec.Cmd, error) {
|
||||
command := "git commit --amend --no-edit --allow-empty"
|
||||
if c.usingGpg() {
|
||||
return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command), nil
|
||||
return c.OSCommand.ShellCommandFromString(command), nil
|
||||
}
|
||||
|
||||
return nil, c.OSCommand.RunCommand(command)
|
||||
}
|
||||
|
||||
// Pull pulls from repo
|
||||
func (c *GitCommand) Pull(ask func(string) string) error {
|
||||
return c.OSCommand.DetectUnamePass("git pull --no-edit", ask)
|
||||
}
|
||||
|
||||
// Push pushes to a branch
|
||||
func (c *GitCommand) Push(branchName string, force bool, upstream string, ask func(string) string) error {
|
||||
func (c *GitCommand) Push(branchName string, force bool, upstream string, args string, promptUserForCredential func(string) string) error {
|
||||
forceFlag := ""
|
||||
if force {
|
||||
forceFlag = "--force-with-lease"
|
||||
@@ -420,18 +522,20 @@ func (c *GitCommand) Push(branchName string, force bool, upstream string, ask fu
|
||||
setUpstreamArg = "--set-upstream " + upstream
|
||||
}
|
||||
|
||||
cmd := fmt.Sprintf("git push --follow-tags %s %s", forceFlag, setUpstreamArg)
|
||||
return c.OSCommand.DetectUnamePass(cmd, ask)
|
||||
cmd := fmt.Sprintf("git push --follow-tags %s %s %s", forceFlag, setUpstreamArg, args)
|
||||
return c.OSCommand.DetectUnamePass(cmd, promptUserForCredential)
|
||||
}
|
||||
|
||||
// CatFile obtains the content of a file
|
||||
func (c *GitCommand) CatFile(fileName string) (string, error) {
|
||||
return c.OSCommand.RunCommandWithOutput("cat %s", c.OSCommand.Quote(fileName))
|
||||
return c.OSCommand.RunCommandWithOutput("%s %s", c.OSCommand.Platform.catCmd, c.OSCommand.Quote(fileName))
|
||||
}
|
||||
|
||||
// StageFile stages a file
|
||||
func (c *GitCommand) StageFile(fileName string) error {
|
||||
return c.OSCommand.RunCommand("git add %s", c.OSCommand.Quote(fileName))
|
||||
// renamed files look like "file1 -> file2"
|
||||
fileNames := strings.Split(fileName, " -> ")
|
||||
return c.OSCommand.RunCommand("git add %s", c.OSCommand.Quote(fileNames[len(fileNames)-1]))
|
||||
}
|
||||
|
||||
// StageAll stages all files
|
||||
@@ -462,17 +566,21 @@ func (c *GitCommand) UnStageFile(fileName string, tracked bool) error {
|
||||
}
|
||||
|
||||
// GitStatus returns the plaintext short status of the repo
|
||||
func (c *GitCommand) GitStatus() (string, error) {
|
||||
return c.OSCommand.RunCommandWithOutput("git status --untracked-files=all --porcelain")
|
||||
type GitStatusOptions struct {
|
||||
NoRenames bool
|
||||
}
|
||||
|
||||
func (c *GitCommand) GitStatus(opts GitStatusOptions) (string, error) {
|
||||
noRenamesFlag := ""
|
||||
if opts.NoRenames {
|
||||
noRenamesFlag = "--no-renames"
|
||||
}
|
||||
return c.OSCommand.RunCommandWithOutput("git status --untracked-files=all --porcelain %s", noRenamesFlag)
|
||||
}
|
||||
|
||||
// IsInMergeState states whether we are still mid-merge
|
||||
func (c *GitCommand) IsInMergeState() (bool, error) {
|
||||
output, err := c.OSCommand.RunCommandWithOutput("git status --untracked-files=all")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return strings.Contains(output, "conclude merge") || strings.Contains(output, "unmerged paths"), nil
|
||||
return c.OSCommand.FileExists(fmt.Sprintf("%s/MERGE_HEAD", c.DotGitDir))
|
||||
}
|
||||
|
||||
// RebaseMode returns "" for non-rebase mode, "normal" for normal rebase
|
||||
@@ -493,8 +601,61 @@ func (c *GitCommand) RebaseMode() (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GitCommand) BeforeAndAfterFileForRename(file *File) (*File, *File, error) {
|
||||
|
||||
if !file.IsRename() {
|
||||
return nil, nil, errors.New("Expected renamed file")
|
||||
}
|
||||
|
||||
// we've got a file that represents a rename from one file to another. Unfortunately
|
||||
// our File abstraction fails to consider this case, so here we will refetch
|
||||
// all files, passing the --no-renames flag and then recursively call the function
|
||||
// again for the before file and after file. At some point we should fix the abstraction itself
|
||||
|
||||
split := strings.Split(file.Name, " -> ")
|
||||
filesWithoutRenames := c.GetStatusFiles(GetStatusFileOptions{NoRenames: true})
|
||||
var beforeFile *File
|
||||
var afterFile *File
|
||||
for _, f := range filesWithoutRenames {
|
||||
if f.Name == split[0] {
|
||||
beforeFile = f
|
||||
}
|
||||
if f.Name == split[1] {
|
||||
afterFile = f
|
||||
}
|
||||
}
|
||||
|
||||
if beforeFile == nil || afterFile == nil {
|
||||
return nil, nil, errors.New("Could not find deleted file or new file for file rename")
|
||||
}
|
||||
|
||||
if beforeFile.IsRename() || afterFile.IsRename() {
|
||||
// probably won't happen but we want to ensure we don't get an infinite loop
|
||||
return nil, nil, errors.New("Nested rename found")
|
||||
}
|
||||
|
||||
return beforeFile, afterFile, nil
|
||||
}
|
||||
|
||||
// DiscardAllFileChanges directly
|
||||
func (c *GitCommand) DiscardAllFileChanges(file *File) error {
|
||||
if file.IsRename() {
|
||||
beforeFile, afterFile, err := c.BeforeAndAfterFileForRename(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.DiscardAllFileChanges(beforeFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.DiscardAllFileChanges(afterFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// if the file isn't tracked, we assume you want to delete it
|
||||
quotedFileName := c.OSCommand.Quote(file.Name)
|
||||
if file.HasStagedChanges || file.HasMergeConflicts {
|
||||
@@ -516,12 +677,17 @@ func (c *GitCommand) DiscardUnstagedFileChanges(file *File) error {
|
||||
}
|
||||
|
||||
// Checkout checks out a branch (or commit), with --force if you set the force arg to true
|
||||
func (c *GitCommand) Checkout(branch string, force bool) error {
|
||||
type CheckoutOptions struct {
|
||||
Force bool
|
||||
EnvVars []string
|
||||
}
|
||||
|
||||
func (c *GitCommand) Checkout(branch string, options CheckoutOptions) error {
|
||||
forceArg := ""
|
||||
if force {
|
||||
if options.Force {
|
||||
forceArg = "--force "
|
||||
}
|
||||
return c.OSCommand.RunCommand("git checkout %s %s", forceArg, branch)
|
||||
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git checkout %s %s", forceArg, branch), RunCommandOptions{EnvVars: options.EnvVars})
|
||||
}
|
||||
|
||||
// PrepareCommitSubProcess prepares a subprocess for `git commit`
|
||||
@@ -538,7 +704,8 @@ func (c *GitCommand) PrepareCommitAmendSubProcess() *exec.Cmd {
|
||||
// Currently it limits the result to 100 commits, but when we get async stuff
|
||||
// working we can do lazy loading
|
||||
func (c *GitCommand) GetBranchGraph(branchName string) (string, error) {
|
||||
return c.OSCommand.RunCommandWithOutput("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium -100 %s", branchName)
|
||||
cmdStr := c.GetBranchGraphCmdStr(branchName)
|
||||
return c.OSCommand.RunCommandWithOutput(cmdStr)
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetUpstreamForBranch(branchName string) (string, error) {
|
||||
@@ -551,41 +718,20 @@ func (c *GitCommand) Ignore(filename string) error {
|
||||
return c.OSCommand.AppendLineToFile(".gitignore", filename)
|
||||
}
|
||||
|
||||
// Show shows the diff of a commit
|
||||
func (c *GitCommand) Show(sha string) (string, error) {
|
||||
show, err := c.OSCommand.RunCommandWithOutput("git show --color --no-renames %s", sha)
|
||||
if err != nil {
|
||||
return "", err
|
||||
func (c *GitCommand) ShowCmdStr(sha string, filterPath string) string {
|
||||
filterPathArg := ""
|
||||
if filterPath != "" {
|
||||
filterPathArg = fmt.Sprintf(" -- %s", c.OSCommand.Quote(filterPath))
|
||||
}
|
||||
return fmt.Sprintf("git show --color=%s --no-renames --stat -p %s %s", c.colorArg(), sha, filterPathArg)
|
||||
}
|
||||
|
||||
// if this is a merge commit, we need to go a step further and get the diff between the two branches we merged
|
||||
revList, err := c.OSCommand.RunCommandWithOutput("git rev-list -1 --merges %s^...%s", sha, sha)
|
||||
if err != nil {
|
||||
// turns out we get an error here when it's the first commit. We'll just return the original show
|
||||
return show, nil
|
||||
func (c *GitCommand) GetBranchGraphCmdStr(branchName string) string {
|
||||
branchLogCmdTemplate := c.Config.GetUserConfig().GetString("git.branchLogCmd")
|
||||
templateValues := map[string]string{
|
||||
"branchName": branchName,
|
||||
}
|
||||
if len(revList) == 0 {
|
||||
return show, nil
|
||||
}
|
||||
|
||||
// we want to pull out 1a6a69a and 3b51d7c from this:
|
||||
// commit ccc771d8b13d5b0d4635db4463556366470fd4f6
|
||||
// Merge: 1a6a69a 3b51d7c
|
||||
lines := utils.SplitLines(show)
|
||||
if len(lines) < 2 {
|
||||
return show, nil
|
||||
}
|
||||
|
||||
secondLineWords := strings.Split(lines[1], " ")
|
||||
if len(secondLineWords) < 3 {
|
||||
return show, nil
|
||||
}
|
||||
|
||||
mergeDiff, err := c.OSCommand.RunCommandWithOutput("git diff --color %s...%s", secondLineWords[1], secondLineWords[2])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return show + mergeDiff, nil
|
||||
return utils.ResolvePlaceholderString(branchLogCmdTemplate, templateValues)
|
||||
}
|
||||
|
||||
// GetRemoteURL returns current repo remote url
|
||||
@@ -606,24 +752,28 @@ func (c *GitCommand) CheckRemoteBranchExists(branch *Branch) bool {
|
||||
|
||||
// Diff returns the diff of a file
|
||||
func (c *GitCommand) Diff(file *File, plain bool, cached bool) string {
|
||||
// for now we assume an error means the file was deleted
|
||||
s, _ := c.OSCommand.RunCommandWithOutput(c.DiffCmdStr(file, plain, cached))
|
||||
return s
|
||||
}
|
||||
|
||||
func (c *GitCommand) DiffCmdStr(file *File, plain bool, cached bool) string {
|
||||
cachedArg := ""
|
||||
trackedArg := "--"
|
||||
colorArg := "--color"
|
||||
colorArg := c.colorArg()
|
||||
split := strings.Split(file.Name, " -> ") // in case of a renamed file we get the new filename
|
||||
fileName := c.OSCommand.Quote(split[len(split)-1])
|
||||
if cached {
|
||||
cachedArg = "--cached"
|
||||
}
|
||||
if !file.Tracked && !file.HasStagedChanges {
|
||||
if !file.Tracked && !file.HasStagedChanges && !cached {
|
||||
trackedArg = "--no-index /dev/null"
|
||||
}
|
||||
if plain {
|
||||
colorArg = ""
|
||||
colorArg = "never"
|
||||
}
|
||||
|
||||
// for now we assume an error means the file was deleted
|
||||
s, _ := c.OSCommand.RunCommandWithOutput("git diff %s %s %s %s", colorArg, cachedArg, trackedArg, fileName)
|
||||
return s
|
||||
return fmt.Sprintf("git diff --color=%s %s %s %s", colorArg, cachedArg, trackedArg, fileName)
|
||||
}
|
||||
|
||||
func (c *GitCommand) ApplyPatch(patch string, flags ...string) error {
|
||||
@@ -641,16 +791,20 @@ func (c *GitCommand) ApplyPatch(patch string, flags ...string) error {
|
||||
return c.OSCommand.RunCommand("git apply %s %s", flagStr, c.OSCommand.Quote(filepath))
|
||||
}
|
||||
|
||||
func (c *GitCommand) FastForward(branchName string, remoteName string, remoteBranchName string) error {
|
||||
return c.OSCommand.RunCommand("git fetch %s %s:%s", remoteName, remoteBranchName, branchName)
|
||||
func (c *GitCommand) FastForward(branchName string, remoteName string, remoteBranchName string, promptUserForCredential func(string) string) error {
|
||||
command := fmt.Sprintf("git fetch %s %s:%s", remoteName, remoteBranchName, branchName)
|
||||
return c.OSCommand.DetectUnamePass(command, promptUserForCredential)
|
||||
}
|
||||
|
||||
func (c *GitCommand) RunSkipEditorCommand(command string) error {
|
||||
cmd := c.OSCommand.ExecutableFromString(command)
|
||||
lazyGitPath := c.OSCommand.GetLazygitPath()
|
||||
cmd.Env = append(
|
||||
cmd.Env,
|
||||
"LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY",
|
||||
"EDITOR="+c.OSCommand.GetLazygitPath(),
|
||||
"GIT_EDITOR="+lazyGitPath,
|
||||
"EDITOR="+lazyGitPath,
|
||||
"VISUAL="+lazyGitPath,
|
||||
)
|
||||
return c.OSCommand.RunExecutable(cmd)
|
||||
}
|
||||
@@ -666,7 +820,10 @@ func (c *GitCommand) GenericMerge(commandType string, command string) error {
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
if !strings.Contains(err.Error(), "no rebase in progress") {
|
||||
return err
|
||||
}
|
||||
c.Log.Warn(err)
|
||||
}
|
||||
|
||||
// sometimes we need to do a sequence of things in a rebase but the user needs to
|
||||
@@ -759,7 +916,7 @@ func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string
|
||||
)
|
||||
|
||||
if overrideEditor {
|
||||
cmd.Env = append(cmd.Env, "EDITOR="+ex)
|
||||
cmd.Env = append(cmd.Env, "GIT_EDITOR="+ex)
|
||||
}
|
||||
|
||||
return cmd, nil
|
||||
@@ -882,7 +1039,7 @@ func (c *GitCommand) CherryPickCommits(commits []*Commit) error {
|
||||
|
||||
// GetCommitFiles get the specified commit files
|
||||
func (c *GitCommand) GetCommitFiles(commitSha string, patchManager *PatchManager) ([]*CommitFile, error) {
|
||||
files, err := c.OSCommand.RunCommandWithOutput("git show --pretty= --name-only --no-renames %s", commitSha)
|
||||
files, err := c.OSCommand.RunCommandWithOutput("git diff-tree --no-commit-id --name-only -r --no-renames %s", commitSha)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -908,11 +1065,17 @@ func (c *GitCommand) GetCommitFiles(commitSha string, patchManager *PatchManager
|
||||
|
||||
// ShowCommitFile get the diff of specified commit file
|
||||
func (c *GitCommand) ShowCommitFile(commitSha, fileName string, plain bool) (string, error) {
|
||||
colorArg := "--color"
|
||||
cmdStr := c.ShowCommitFileCmdStr(commitSha, fileName, plain)
|
||||
return c.OSCommand.RunCommandWithOutput(cmdStr)
|
||||
}
|
||||
|
||||
func (c *GitCommand) ShowCommitFileCmdStr(commitSha, fileName string, plain bool) string {
|
||||
colorArg := c.colorArg()
|
||||
if plain {
|
||||
colorArg = ""
|
||||
colorArg = "never"
|
||||
}
|
||||
return c.OSCommand.RunCommandWithOutput("git show --no-renames %s %s -- %s", colorArg, commitSha, fileName)
|
||||
|
||||
return fmt.Sprintf("git show --no-renames --color=%s %s -- %s", colorArg, commitSha, fileName)
|
||||
}
|
||||
|
||||
// CheckoutFile checks out the file for the given commit
|
||||
@@ -956,6 +1119,11 @@ func (c *GitCommand) DiscardAnyUnstagedFileChanges() error {
|
||||
return c.OSCommand.RunCommand("git checkout -- .")
|
||||
}
|
||||
|
||||
// RemoveTrackedFiles will delete the given file(s) even if they are currently tracked
|
||||
func (c *GitCommand) RemoveTrackedFiles(name string) error {
|
||||
return c.OSCommand.RunCommand("git rm -r --cached %s", name)
|
||||
}
|
||||
|
||||
// RemoveUntrackedFiles runs `git clean -fd`
|
||||
func (c *GitCommand) RemoveUntrackedFiles() error {
|
||||
return c.OSCommand.RunCommand("git clean -fd")
|
||||
@@ -971,11 +1139,6 @@ func (c *GitCommand) ResetSoft(ref string) error {
|
||||
return c.OSCommand.RunCommand("git reset --soft " + ref)
|
||||
}
|
||||
|
||||
// DiffCommits show diff between commits
|
||||
func (c *GitCommand) DiffCommits(sha1, sha2 string) (string, error) {
|
||||
return c.OSCommand.RunCommandWithOutput("git diff --color %s %s", sha1, sha2)
|
||||
}
|
||||
|
||||
// CreateFixupCommit creates a commit that fixes up a previous commit
|
||||
func (c *GitCommand) CreateFixupCommit(sha string) error {
|
||||
return c.OSCommand.RunCommand("git commit --fixup=%s", sha)
|
||||
@@ -1018,7 +1181,7 @@ func (c *GitCommand) StashSaveStagedChanges(message string) error {
|
||||
// if you had staged an untracked file, that will now appear as 'AD' in git status
|
||||
// meaning it's deleted in your working tree but added in your index. Given that it's
|
||||
// now safely stashed, we need to remove it.
|
||||
files := c.GetStatusFiles()
|
||||
files := c.GetStatusFiles(GetStatusFileOptions{})
|
||||
for _, file := range files {
|
||||
if file.ShortStatus == "AD" {
|
||||
if err := c.UnStageFile(file.Name, false); err != nil {
|
||||
@@ -1098,10 +1261,6 @@ func (c *GitCommand) CreateLightweightTag(tagName string, commitSha string) erro
|
||||
return c.OSCommand.RunCommand("git tag %s %s", tagName, commitSha)
|
||||
}
|
||||
|
||||
func (c *GitCommand) ShowTag(tagName string) (string, error) {
|
||||
return c.OSCommand.RunCommandWithOutput("git tag -n99 %s", tagName)
|
||||
}
|
||||
|
||||
func (c *GitCommand) DeleteTag(tagName string) error {
|
||||
return c.OSCommand.RunCommand("git tag -d %s", tagName)
|
||||
}
|
||||
@@ -1114,27 +1273,97 @@ func (c *GitCommand) FetchRemote(remoteName string) error {
|
||||
return c.OSCommand.RunCommand("git fetch %s", remoteName)
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetReflogCommits() ([]*Commit, error) {
|
||||
output, err := c.OSCommand.RunCommandWithOutput("git reflog")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// GetReflogCommits only returns the new reflog commits since the given lastReflogCommit
|
||||
// if none is passed (i.e. it's value is nil) then we get all the reflog commits
|
||||
|
||||
func (c *GitCommand) GetReflogCommits(lastReflogCommit *Commit, filterPath string) ([]*Commit, bool, error) {
|
||||
commits := make([]*Commit, 0)
|
||||
re := regexp.MustCompile(`(\w+).*HEAD@\{([^\}]+)\}: (.*)`)
|
||||
|
||||
filterPathArg := ""
|
||||
if filterPath != "" {
|
||||
filterPathArg = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(filterPath))
|
||||
}
|
||||
|
||||
lines := strings.Split(strings.TrimSpace(output), "\n")
|
||||
commits := make([]*Commit, len(lines))
|
||||
re := regexp.MustCompile(`(\w+).*HEAD@\{\d+\}: (.*)`)
|
||||
for i, line := range lines {
|
||||
cmd := c.OSCommand.ExecutableFromString(fmt.Sprintf("git reflog --abbrev=20 --date=unix %s", filterPathArg))
|
||||
onlyObtainedNewReflogCommits := false
|
||||
err := RunLineOutputCmd(cmd, func(line string) (bool, error) {
|
||||
match := re.FindStringSubmatch(line)
|
||||
if len(match) == 1 {
|
||||
continue
|
||||
if len(match) <= 1 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
commits[i] = &Commit{
|
||||
Sha: match[1],
|
||||
Name: match[2],
|
||||
Status: "reflog",
|
||||
unixTimestamp, _ := strconv.Atoi(match[2])
|
||||
|
||||
commit := &Commit{
|
||||
Sha: match[1],
|
||||
Name: match[3],
|
||||
UnixTimestamp: int64(unixTimestamp),
|
||||
Status: "reflog",
|
||||
}
|
||||
|
||||
if lastReflogCommit != nil && commit.Sha == lastReflogCommit.Sha && commit.UnixTimestamp == lastReflogCommit.UnixTimestamp {
|
||||
onlyObtainedNewReflogCommits = true
|
||||
// after this point we already have these reflogs loaded so we'll simply return the new ones
|
||||
return true, nil
|
||||
}
|
||||
|
||||
commits = append(commits, commit)
|
||||
return false, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
return commits, nil
|
||||
return commits, onlyObtainedNewReflogCommits, nil
|
||||
}
|
||||
|
||||
func (c *GitCommand) ConfiguredPager() string {
|
||||
if os.Getenv("GIT_PAGER") != "" {
|
||||
return os.Getenv("GIT_PAGER")
|
||||
}
|
||||
if os.Getenv("PAGER") != "" {
|
||||
return os.Getenv("PAGER")
|
||||
}
|
||||
output, err := c.OSCommand.RunCommandWithOutput("git config --get-all core.pager")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
trimmedOutput := strings.TrimSpace(output)
|
||||
return strings.Split(trimmedOutput, "\n")[0]
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetPager(width int) string {
|
||||
useConfig := c.Config.GetUserConfig().GetBool("git.paging.useConfig")
|
||||
if useConfig {
|
||||
pager := c.ConfiguredPager()
|
||||
return strings.Split(pager, "| less")[0]
|
||||
}
|
||||
|
||||
templateValues := map[string]string{
|
||||
"columnWidth": strconv.Itoa(width/2 - 6),
|
||||
}
|
||||
|
||||
pagerTemplate := c.Config.GetUserConfig().GetString("git.paging.pager")
|
||||
return utils.ResolvePlaceholderString(pagerTemplate, templateValues)
|
||||
}
|
||||
|
||||
func (c *GitCommand) colorArg() string {
|
||||
return c.Config.GetUserConfig().GetString("git.paging.colorArg")
|
||||
}
|
||||
|
||||
func (c *GitCommand) RenameBranch(oldName string, newName string) error {
|
||||
return c.OSCommand.RunCommand("git branch --move %s %s", oldName, newName)
|
||||
}
|
||||
|
||||
func (c *GitCommand) WorkingTreeState() string {
|
||||
rebaseMode, _ := c.RebaseMode()
|
||||
if rebaseMode != "" {
|
||||
return "rebasing"
|
||||
}
|
||||
merging, _ := c.IsInMergeState()
|
||||
if merging {
|
||||
return "merging"
|
||||
}
|
||||
return "normal"
|
||||
}
|
||||
|
||||
@@ -5,14 +5,16 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
gogit "github.com/go-git/go-git/v5"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
gogit "gopkg.in/src-d/go-git.v4"
|
||||
)
|
||||
|
||||
type fileInfoMock struct {
|
||||
@@ -293,12 +295,10 @@ func TestGitCommandGetStashEntries(t *testing.T) {
|
||||
{
|
||||
0,
|
||||
"WIP on add-pkg-commands-test: 55c6af2 increase parallel build",
|
||||
"WIP on add-pkg-commands-test: 55c6af2 increase parallel build",
|
||||
},
|
||||
{
|
||||
1,
|
||||
"WIP on master: bb86a3f update github template",
|
||||
"WIP on master: bb86a3f update github template",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -313,26 +313,11 @@ func TestGitCommandGetStashEntries(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.command = s.command
|
||||
|
||||
s.test(gitCmd.GetStashEntries())
|
||||
s.test(gitCmd.GetStashEntries(""))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandGetStashEntryDiff is a function.
|
||||
func TestGitCommandGetStashEntryDiff(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", "show", "-p", "--color", "stash@{1}"}, args)
|
||||
|
||||
return exec.Command("echo")
|
||||
}
|
||||
|
||||
_, err := gitCmd.GetStashEntryDiff(1)
|
||||
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// TestGitCommandGetStatusFiles is a function.
|
||||
func TestGitCommandGetStatusFiles(t *testing.T) {
|
||||
type scenario struct {
|
||||
@@ -435,7 +420,7 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.command = s.command
|
||||
|
||||
s.test(gitCmd.GetStatusFiles())
|
||||
s.test(gitCmd.GetStatusFiles(GetStatusFileOptions{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -556,7 +541,7 @@ func TestGitCommandMergeStatusFiles(t *testing.T) {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
s.test(gitCmd.MergeStatusFiles(s.oldFiles, s.newFiles))
|
||||
s.test(gitCmd.MergeStatusFiles(s.oldFiles, s.newFiles, nil))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -642,7 +627,7 @@ func TestGitCommandResetToCommit(t *testing.T) {
|
||||
return exec.Command("echo")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.ResetToCommit("78976bc", "hard"))
|
||||
assert.NoError(t, gitCmd.ResetToCommit("78976bc", "hard", RunCommandOptions{}))
|
||||
}
|
||||
|
||||
// TestGitCommandNewBranch is a function.
|
||||
@@ -650,12 +635,12 @@ 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"}, args)
|
||||
assert.EqualValues(t, []string{"checkout", "-b", "test", "master"}, args)
|
||||
|
||||
return exec.Command("echo")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.NewBranch("test"))
|
||||
assert.NoError(t, gitCmd.NewBranch("test", "master"))
|
||||
}
|
||||
|
||||
// TestGitCommandDeleteBranch is a function.
|
||||
@@ -718,7 +703,7 @@ func TestGitCommandMerge(t *testing.T) {
|
||||
return exec.Command("echo")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.Merge("test"))
|
||||
assert.NoError(t, gitCmd.Merge("test", MergeOpts{}))
|
||||
}
|
||||
|
||||
// TestGitCommandUsingGpg is a function.
|
||||
@@ -830,7 +815,7 @@ func TestGitCommandCommit(t *testing.T) {
|
||||
"Commit using gpg",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "bash", cmd)
|
||||
assert.EqualValues(t, []string{"-c", `git commit -m 'test'`}, args)
|
||||
assert.EqualValues(t, []string{"-c", "git commit -m \"test\""}, args)
|
||||
|
||||
return exec.Command("echo")
|
||||
},
|
||||
@@ -1030,7 +1015,7 @@ func TestGitCommandPush(t *testing.T) {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.command = s.command
|
||||
err := gitCmd.Push("test", s.forcePush, "", func(passOrUname string) string {
|
||||
err := gitCmd.Push("test", s.forcePush, "", "", func(passOrUname string) string {
|
||||
return "\n"
|
||||
})
|
||||
s.test(err)
|
||||
@@ -1038,11 +1023,18 @@ func TestGitCommandPush(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandCatFile is a function.
|
||||
// TestGitCommandCatFile tests emitting a file using commands, where commands vary by OS.
|
||||
func TestGitCommandCatFile(t *testing.T) {
|
||||
var osCmd string
|
||||
switch os := runtime.GOOS; os {
|
||||
case "windows":
|
||||
osCmd = "type"
|
||||
default:
|
||||
osCmd = "cat"
|
||||
}
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "cat", cmd)
|
||||
assert.EqualValues(t, osCmd, cmd)
|
||||
assert.EqualValues(t, []string{"test.txt"}, args)
|
||||
|
||||
return exec.Command("echo", "-n", "test")
|
||||
@@ -1113,75 +1105,6 @@ func TestGitCommandUnstageFile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandIsInMergeState is a function.
|
||||
func TestGitCommandIsInMergeState(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(bool, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"An error occurred when running status command",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"status", "--untracked-files=all"}, args)
|
||||
|
||||
return exec.Command("test")
|
||||
},
|
||||
func(isInMergeState bool, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.False(t, isInMergeState)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Is not in merge state",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"status", "--untracked-files=all"}, args)
|
||||
return exec.Command("echo")
|
||||
},
|
||||
func(isInMergeState bool, err error) {
|
||||
assert.False(t, isInMergeState)
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Command output contains conclude merge",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"status", "--untracked-files=all"}, args)
|
||||
return exec.Command("echo", "'conclude merge'")
|
||||
},
|
||||
func(isInMergeState bool, err error) {
|
||||
assert.True(t, isInMergeState)
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Command output contains unmerged paths",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"status", "--untracked-files=all"}, args)
|
||||
return exec.Command("echo", "'unmerged paths'")
|
||||
},
|
||||
func(isInMergeState bool, err error) {
|
||||
assert.True(t, isInMergeState)
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.command = s.command
|
||||
s.test(gitCmd.IsInMergeState())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandDiscardAllFileChanges is a function.
|
||||
func TestGitCommandDiscardAllFileChanges(t *testing.T) {
|
||||
type scenario struct {
|
||||
@@ -1411,66 +1334,6 @@ func TestGitCommandDiscardAllFileChanges(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandShow is a function.
|
||||
func TestGitCommandShow(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
arg string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(string, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"regular commit",
|
||||
"456abcde",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git show --color --no-renames 456abcde",
|
||||
Replace: "echo \"commit ccc771d8b13d5b0d4635db4463556366470fd4f6\nblah\"",
|
||||
},
|
||||
{
|
||||
Expect: "git rev-list -1 --merges 456abcde^...456abcde",
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
func(result string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "commit ccc771d8b13d5b0d4635db4463556366470fd4f6\nblah\n", result)
|
||||
},
|
||||
},
|
||||
{
|
||||
"merge commit",
|
||||
"456abcde",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git show --color --no-renames 456abcde",
|
||||
Replace: "echo \"commit ccc771d8b13d5b0d4635db4463556366470fd4f6\nMerge: 1a6a69a 3b51d7c\"",
|
||||
},
|
||||
{
|
||||
Expect: "git rev-list -1 --merges 456abcde^...456abcde",
|
||||
Replace: "echo aa30e006433628ba9281652952b34d8aacda9c01",
|
||||
},
|
||||
{
|
||||
Expect: "git diff --color 1a6a69a...3b51d7c",
|
||||
Replace: "echo blah",
|
||||
},
|
||||
}),
|
||||
func(result string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "commit ccc771d8b13d5b0d4635db4463556366470fd4f6\nMerge: 1a6a69a 3b51d7c\nblah\n", result)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
gitCmd.OSCommand.command = s.command
|
||||
s.test(gitCmd.Show(s.arg))
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandCheckout is a function.
|
||||
func TestGitCommandCheckout(t *testing.T) {
|
||||
type scenario struct {
|
||||
@@ -1513,7 +1376,7 @@ func TestGitCommandCheckout(t *testing.T) {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.command = s.command
|
||||
s.test(gitCmd.Checkout("test", s.force))
|
||||
s.test(gitCmd.Checkout("test", CheckoutOptions{Force: s.force}))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1523,11 +1386,9 @@ 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", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium", "-100", "test"}, args)
|
||||
|
||||
assert.EqualValues(t, []string{"log", "--graph", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium", "test", "--"}, args)
|
||||
return exec.Command("echo")
|
||||
}
|
||||
|
||||
_, err := gitCmd.GetBranchGraph("test")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@@ -1547,7 +1408,7 @@ func TestGitCommandDiff(t *testing.T) {
|
||||
"Default case",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"diff", "--color", "--", "test.txt"}, args)
|
||||
assert.EqualValues(t, []string{"diff", "--color=always", "--", "test.txt"}, args)
|
||||
|
||||
return exec.Command("echo")
|
||||
},
|
||||
@@ -1563,7 +1424,7 @@ func TestGitCommandDiff(t *testing.T) {
|
||||
"cached",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"diff", "--color", "--cached", "--", "test.txt"}, args)
|
||||
assert.EqualValues(t, []string{"diff", "--color=always", "--cached", "--", "test.txt"}, args)
|
||||
|
||||
return exec.Command("echo")
|
||||
},
|
||||
@@ -1579,7 +1440,7 @@ func TestGitCommandDiff(t *testing.T) {
|
||||
"plain",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"diff", "--", "test.txt"}, args)
|
||||
assert.EqualValues(t, []string{"diff", "--color=never", "--", "test.txt"}, args)
|
||||
|
||||
return exec.Command("echo")
|
||||
},
|
||||
@@ -1595,7 +1456,7 @@ func TestGitCommandDiff(t *testing.T) {
|
||||
"File not tracked and file has no staged changes",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"diff", "--color", "--no-index", "/dev/null", "test.txt"}, args)
|
||||
assert.EqualValues(t, []string{"diff", "--color=always", "--no-index", "/dev/null", "test.txt"}, args)
|
||||
|
||||
return exec.Command("echo")
|
||||
},
|
||||
@@ -1623,7 +1484,7 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(string, error)
|
||||
test func(string, string, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
@@ -1633,9 +1494,10 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
|
||||
assert.Equal(t, "git", cmd)
|
||||
return exec.Command("echo", "master")
|
||||
},
|
||||
func(output string, err error) {
|
||||
func(name string, displayname string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "master", output)
|
||||
assert.EqualValues(t, "master", name)
|
||||
assert.EqualValues(t, "master", displayname)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1654,9 +1516,32 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
|
||||
|
||||
return nil
|
||||
},
|
||||
func(output string, err error) {
|
||||
func(name string, displayname string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "master", output)
|
||||
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 exec.Command("test")
|
||||
case "branch":
|
||||
assert.EqualValues(t, []string{"branch", "--contains"}, args)
|
||||
return exec.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)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1665,9 +1550,10 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
|
||||
assert.Equal(t, "git", cmd)
|
||||
return exec.Command("test")
|
||||
},
|
||||
func(output string, err error) {
|
||||
func(name string, displayname string, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.EqualValues(t, "", output)
|
||||
assert.EqualValues(t, "", name)
|
||||
assert.EqualValues(t, "", displayname)
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1945,7 +1831,7 @@ func TestGitCommandShowCommitFile(t *testing.T) {
|
||||
"hello.txt",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git show --no-renames 123456 -- hello.txt",
|
||||
Expect: "git show --no-renames --color=never 123456 -- hello.txt",
|
||||
Replace: "echo -n hello",
|
||||
},
|
||||
}),
|
||||
@@ -1981,7 +1867,7 @@ func TestGitCommandGetCommitFiles(t *testing.T) {
|
||||
"123456",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git show --pretty= --name-only --no-renames 123456",
|
||||
Expect: "git diff-tree --no-commit-id --name-only -r --no-renames 123456",
|
||||
Replace: "echo 'hello\nworld'",
|
||||
},
|
||||
}),
|
||||
@@ -2176,6 +2062,44 @@ func TestGitCommandCreateFixupCommit(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
func TestFindDotGitDir(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -22,6 +24,7 @@ import (
|
||||
// Platform stores the os state
|
||||
type Platform struct {
|
||||
os string
|
||||
catCmd string
|
||||
shell string
|
||||
shellArg string
|
||||
escapedQuote string
|
||||
@@ -36,6 +39,7 @@ type OSCommand struct {
|
||||
Platform *Platform
|
||||
Config config.AppConfigurer
|
||||
command func(string, ...string) *exec.Cmd
|
||||
beforeExecuteCmd func(*exec.Cmd)
|
||||
getGlobalGitConfig func(string) (string, error)
|
||||
getenv func(string) string
|
||||
}
|
||||
@@ -47,6 +51,7 @@ func NewOSCommand(log *logrus.Entry, config config.AppConfigurer) *OSCommand {
|
||||
Platform: getPlatform(),
|
||||
Config: config,
|
||||
command: exec.Command,
|
||||
beforeExecuteCmd: func(*exec.Cmd) {},
|
||||
getGlobalGitConfig: gitconfig.Global,
|
||||
getenv: os.Getenv,
|
||||
}
|
||||
@@ -58,6 +63,26 @@ func (c *OSCommand) SetCommand(cmd func(string, ...string) *exec.Cmd) {
|
||||
c.command = cmd
|
||||
}
|
||||
|
||||
func (c *OSCommand) SetBeforeExecuteCmd(cmd func(*exec.Cmd)) {
|
||||
c.beforeExecuteCmd = cmd
|
||||
}
|
||||
|
||||
type RunCommandOptions struct {
|
||||
EnvVars []string
|
||||
}
|
||||
|
||||
func (c *OSCommand) RunCommandWithOutputWithOptions(command string, options RunCommandOptions) (string, error) {
|
||||
c.Log.WithField("command", command).Info("RunCommand")
|
||||
cmd := c.ExecutableFromString(command)
|
||||
cmd.Env = append(cmd.Env, options.EnvVars...)
|
||||
return sanitisedCommandOutput(cmd.CombinedOutput())
|
||||
}
|
||||
|
||||
func (c *OSCommand) RunCommandWithOptions(command string, options RunCommandOptions) error {
|
||||
_, err := c.RunCommandWithOutputWithOptions(command, options)
|
||||
return err
|
||||
}
|
||||
|
||||
// RunCommandWithOutput wrapper around commands returning their output and error
|
||||
// NOTE: If you don't pass any formatArgs we'll just use the command directly,
|
||||
// however there's a bizarre compiler error/warning when you pass in a formatString
|
||||
@@ -76,6 +101,7 @@ func (c *OSCommand) RunCommandWithOutput(formatString string, formatArgs ...inte
|
||||
|
||||
// RunExecutableWithOutput runs an executable file and returns its output
|
||||
func (c *OSCommand) RunExecutableWithOutput(cmd *exec.Cmd) (string, error) {
|
||||
c.beforeExecuteCmd(cmd)
|
||||
return sanitisedCommandOutput(cmd.CombinedOutput())
|
||||
}
|
||||
|
||||
@@ -93,28 +119,43 @@ func (c *OSCommand) ExecutableFromString(commandStr string) *exec.Cmd {
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ShellCommandFromString takes a string like `git commit` and returns an executable shell command for it
|
||||
func (c *OSCommand) ShellCommandFromString(commandStr string) *exec.Cmd {
|
||||
quotedCommand := ""
|
||||
// Windows does not seem to like quotes around the command
|
||||
if c.Platform.os == "windows" {
|
||||
quotedCommand = commandStr
|
||||
} else {
|
||||
quotedCommand = strconv.Quote(commandStr)
|
||||
}
|
||||
|
||||
shellCommand := fmt.Sprintf("%s %s %s", c.Platform.shell, c.Platform.shellArg, quotedCommand)
|
||||
return c.ExecutableFromString(shellCommand)
|
||||
}
|
||||
|
||||
// RunCommandWithOutputLive runs RunCommandWithOutputLiveWrapper
|
||||
func (c *OSCommand) RunCommandWithOutputLive(command string, output func(string) string) error {
|
||||
return RunCommandWithOutputLiveWrapper(c, command, output)
|
||||
}
|
||||
|
||||
// DetectUnamePass detect a username / password question in a command
|
||||
// ask is a function that gets executen when this function detect you need to fillin a password
|
||||
// The ask argument will be "username" or "password" and expects the user's password or username back
|
||||
func (c *OSCommand) DetectUnamePass(command string, ask func(string) string) error {
|
||||
// promptUserForCredential is a function that gets executed when this function detect you need to fillin a password
|
||||
// The promptUserForCredential argument will be "username" or "password" and expects the user's password or username back
|
||||
func (c *OSCommand) DetectUnamePass(command string, promptUserForCredential func(string) string) error {
|
||||
ttyText := ""
|
||||
errMessage := c.RunCommandWithOutputLive(command, func(word string) string {
|
||||
ttyText = ttyText + " " + word
|
||||
|
||||
prompts := map[string]string{
|
||||
"password": `Password\s*for\s*'.+':`,
|
||||
"username": `Username\s*for\s*'.+':`,
|
||||
`.+'s password:`: "password",
|
||||
`Password\s*for\s*'.+':`: "password",
|
||||
`Username\s*for\s*'.+':`: "username",
|
||||
}
|
||||
|
||||
for askFor, pattern := range prompts {
|
||||
for pattern, askFor := range prompts {
|
||||
if match, _ := regexp.MatchString(pattern, ttyText); match {
|
||||
ttyText = ""
|
||||
return ask(askFor)
|
||||
return promptUserForCredential(askFor)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,6 +349,7 @@ func (c *OSCommand) FileExists(path string) (bool, error) {
|
||||
// this is useful if you need to give your command some environment variables
|
||||
// before running it
|
||||
func (c *OSCommand) RunPreparedCommand(cmd *exec.Cmd) error {
|
||||
c.beforeExecuteCmd(cmd)
|
||||
out, err := cmd.CombinedOutput()
|
||||
outString := string(out)
|
||||
c.Log.Info(outString)
|
||||
@@ -393,3 +435,50 @@ func (c *OSCommand) PipeCommands(commandStrings ...string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Kill(cmd *exec.Cmd) error {
|
||||
if cmd.Process == nil {
|
||||
// somebody got to it before we were able to, poor bastard
|
||||
return nil
|
||||
}
|
||||
return cmd.Process.Kill()
|
||||
}
|
||||
|
||||
func RunLineOutputCmd(cmd *exec.Cmd, onLine func(line string) (bool, error)) error {
|
||||
stdoutPipe, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(stdoutPipe)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
stop, err := onLine(line)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if stop {
|
||||
cmd.Process.Kill()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cmd.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *OSCommand) CopyToClipboard(str string) error {
|
||||
commandTemplate := c.Config.GetUserConfig().GetString("os.copyToClipboardCommand")
|
||||
templateValues := map[string]string{
|
||||
"str": c.Quote(str),
|
||||
}
|
||||
|
||||
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
|
||||
|
||||
return c.RunCommand(command)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
func getPlatform() *Platform {
|
||||
return &Platform{
|
||||
os: runtime.GOOS,
|
||||
catCmd: "cat",
|
||||
shell: "bash",
|
||||
shellArg: "-c",
|
||||
escapedQuote: "'",
|
||||
|
||||
@@ -3,6 +3,7 @@ package commands
|
||||
func getPlatform() *Platform {
|
||||
return &Platform{
|
||||
os: "windows",
|
||||
catCmd: "type",
|
||||
shell: "cmd",
|
||||
shellArg: "/c",
|
||||
escapedQuote: `\"`,
|
||||
|
||||
@@ -23,7 +23,7 @@ type PatchManager struct {
|
||||
ApplyPatch applyPatchFunc
|
||||
}
|
||||
|
||||
// NewPatchManager returns a new PatchModifier
|
||||
// NewPatchManager returns a new PatchManager
|
||||
func NewPatchManager(log *logrus.Entry, applyPatch applyPatchFunc) *PatchManager {
|
||||
return &PatchManager{
|
||||
Log: log,
|
||||
@@ -31,7 +31,7 @@ func NewPatchManager(log *logrus.Entry, applyPatch applyPatchFunc) *PatchManager
|
||||
}
|
||||
}
|
||||
|
||||
// NewPatchManager returns a new PatchModifier
|
||||
// NewPatchManager returns a new PatchManager
|
||||
func (p *PatchManager) Start(commitSha string, diffMap map[string]string) {
|
||||
p.CommitSha = commitSha
|
||||
p.fileInfoMap = map[string]*fileInfo{}
|
||||
@@ -101,8 +101,7 @@ func (p *PatchManager) RenderPlainPatchForFile(filename string, reverse bool, ke
|
||||
return info.diff
|
||||
case PART:
|
||||
// generate a new diff with just the selected lines
|
||||
m := NewPatchModifier(p.Log, filename, info.diff)
|
||||
return m.ModifiedPatchForLines(info.includedLineIndices, reverse, keepOriginalHeader)
|
||||
return ModifiedPatchForLines(p.Log, filename, info.diff, info.includedLineIndices, reverse, keepOriginalHeader)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -258,3 +258,8 @@ func ModifiedPatchForRange(log *logrus.Entry, filename string, diffText string,
|
||||
p := NewPatchModifier(log, filename, diffText)
|
||||
return p.ModifiedPatchForRange(firstLineIdx, lastLineIdx, reverse, keepOriginalHeader)
|
||||
}
|
||||
|
||||
func ModifiedPatchForLines(log *logrus.Entry, filename string, diffText string, includedLineIndices []int, reverse bool, keepOriginalHeader bool) string {
|
||||
p := NewPatchModifier(log, filename, diffText)
|
||||
return p.ModifiedPatchForLines(includedLineIndices, reverse, keepOriginalHeader)
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ func coloredString(colorAttr color.Attribute, str string, selected bool, include
|
||||
var cl *color.Color
|
||||
attributes := []color.Attribute{colorAttr}
|
||||
if selected {
|
||||
attributes = append(attributes, color.BgBlue)
|
||||
attributes = append(attributes, theme.SelectedRangeBgColor)
|
||||
}
|
||||
cl = color.New(attributes...)
|
||||
var clIncluded *color.Color
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package commands
|
||||
|
||||
import "github.com/go-errors/errors"
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/go-errors/errors"
|
||||
)
|
||||
|
||||
// DeletePatchesFromCommit applies a patch in reverse for a commit
|
||||
func (c *GitCommand) DeletePatchesFromCommit(commits []*Commit, commitIndex int, p *PatchManager) error {
|
||||
@@ -131,14 +134,22 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*Commit, sourceCommitId
|
||||
return c.GenericMerge("rebase", "continue")
|
||||
}
|
||||
|
||||
func (c *GitCommand) PullPatchIntoIndex(commits []*Commit, commitIdx int, p *PatchManager) error {
|
||||
func (c *GitCommand) PullPatchIntoIndex(commits []*Commit, commitIdx int, p *PatchManager, stash bool) error {
|
||||
if stash {
|
||||
if err := c.StashSave(c.Tr.SLocalize("StashPrefix") + commits[commitIdx].Sha); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.ApplyPatches(true); err != nil {
|
||||
if err := c.GenericMerge("rebase", "abort"); err != nil {
|
||||
return err
|
||||
if c.WorkingTreeState() == "rebasing" {
|
||||
if err := c.GenericMerge("rebase", "abort"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -155,15 +166,63 @@ func (c *GitCommand) PullPatchIntoIndex(commits []*Commit, commitIdx int, p *Pat
|
||||
c.onSuccessfulContinue = func() error {
|
||||
// add patches to index
|
||||
if err := p.ApplyPatches(false); err != nil {
|
||||
if err := c.GenericMerge("rebase", "abort"); err != nil {
|
||||
return err
|
||||
if c.WorkingTreeState() == "rebasing" {
|
||||
if err := c.GenericMerge("rebase", "abort"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if stash {
|
||||
if err := c.StashDo(0, "apply"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
c.PatchManager.Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.GenericMerge("rebase", "continue")
|
||||
}
|
||||
|
||||
func (c *GitCommand) PullPatchIntoNewCommit(commits []*Commit, commitIdx int, p *PatchManager) error {
|
||||
if err := c.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.ApplyPatches(true); err != nil {
|
||||
if err := c.GenericMerge("rebase", "abort"); err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// amend the commit
|
||||
if _, err := c.AmendHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add patches to index
|
||||
if err := p.ApplyPatches(false); err != nil {
|
||||
if err := c.GenericMerge("rebase", "abort"); err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
head_message, _ := c.GetHeadCommitMessage()
|
||||
new_message := fmt.Sprintf("Split from \"%s\"", head_message)
|
||||
_, err := c.Commit(new_message, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.onSuccessfulContinue != nil {
|
||||
return errors.New("You are midway through another rebase operation. Please abort to start again")
|
||||
}
|
||||
|
||||
c.PatchManager.Reset()
|
||||
return c.GenericMerge("rebase", "continue")
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
)
|
||||
|
||||
// Service is a service that repository is on (Github, Bitbucket, ...)
|
||||
@@ -26,27 +27,63 @@ type RepoInformation struct {
|
||||
Repository string
|
||||
}
|
||||
|
||||
func getServices() []*Service {
|
||||
return []*Service{
|
||||
{
|
||||
Name: "github.com",
|
||||
PullRequestURL: "https://github.com/%s/%s/compare/%s?expand=1",
|
||||
},
|
||||
{
|
||||
Name: "bitbucket.org",
|
||||
PullRequestURL: "https://bitbucket.org/%s/%s/pull-requests/new?source=%s&t=1",
|
||||
},
|
||||
{
|
||||
Name: "gitlab.com",
|
||||
PullRequestURL: "https://gitlab.com/%s/%s/merge_requests/new?merge_request[source_branch]=%s",
|
||||
},
|
||||
// NewService builds a Service based on the host type
|
||||
func NewService(typeName string, repositoryDomain string, siteDomain string) *Service {
|
||||
var service *Service
|
||||
|
||||
switch typeName {
|
||||
case "github":
|
||||
service = &Service{
|
||||
Name: repositoryDomain,
|
||||
PullRequestURL: fmt.Sprintf("https://%s%s", siteDomain, "/%s/%s/compare/%s?expand=1"),
|
||||
}
|
||||
case "bitbucket":
|
||||
service = &Service{
|
||||
Name: repositoryDomain,
|
||||
PullRequestURL: fmt.Sprintf("https://%s%s", siteDomain, "/%s/%s/pull-requests/new?source=%s&t=1"),
|
||||
}
|
||||
case "gitlab":
|
||||
service = &Service{
|
||||
Name: repositoryDomain,
|
||||
PullRequestURL: fmt.Sprintf("https://%s%s", siteDomain, "/%s/%s/merge_requests/new?merge_request[source_branch]=%s"),
|
||||
}
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
func getServices(config config.AppConfigurer) []*Service {
|
||||
services := []*Service{
|
||||
NewService("github", "github.com", "github.com"),
|
||||
NewService("bitbucket", "bitbucket.org", "bitbucket.org"),
|
||||
NewService("gitlab", "gitlab.com", "gitlab.com"),
|
||||
}
|
||||
|
||||
configServices := config.GetUserConfig().GetStringMapString("services")
|
||||
|
||||
for repoDomain, typeAndDomain := range configServices {
|
||||
splitData := strings.Split(typeAndDomain, ":")
|
||||
if len(splitData) != 2 {
|
||||
// TODO log this misconfiguration
|
||||
continue
|
||||
}
|
||||
|
||||
service := NewService(splitData[0], repoDomain, splitData[1])
|
||||
if service == nil {
|
||||
// TODO log this unsupported service
|
||||
continue
|
||||
}
|
||||
|
||||
services = append(services, service)
|
||||
}
|
||||
|
||||
return services
|
||||
}
|
||||
|
||||
// NewPullRequest creates new instance of PullRequest
|
||||
func NewPullRequest(gitCommand *GitCommand) *PullRequest {
|
||||
return &PullRequest{
|
||||
GitServices: getServices(),
|
||||
GitServices: getServices(gitCommand.Config),
|
||||
GitCommand: gitCommand,
|
||||
}
|
||||
}
|
||||
@@ -85,7 +122,7 @@ func getRepoInfoFromURL(url string) *RepoInformation {
|
||||
|
||||
if isHTTP {
|
||||
splits := strings.Split(url, "/")
|
||||
owner := splits[len(splits)-2]
|
||||
owner := strings.Join(splits[3:len(splits)-1], "/")
|
||||
repo := strings.TrimSuffix(splits[len(splits)-1], ".git")
|
||||
|
||||
return &RepoInformation{
|
||||
@@ -96,8 +133,8 @@ func getRepoInfoFromURL(url string) *RepoInformation {
|
||||
|
||||
tmpSplit := strings.Split(url, ":")
|
||||
splits := strings.Split(tmpSplit[1], "/")
|
||||
owner := splits[0]
|
||||
repo := strings.TrimSuffix(splits[1], ".git")
|
||||
owner := strings.Join(splits[0:len(splits)-1], "/")
|
||||
repo := strings.TrimSuffix(splits[len(splits)-1], ".git")
|
||||
|
||||
return &RepoInformation{
|
||||
Owner: owner,
|
||||
|
||||
@@ -147,6 +147,13 @@ func TestCreatePullRequest(t *testing.T) {
|
||||
gitCommand := NewDummyGitCommand()
|
||||
gitCommand.OSCommand.command = s.command
|
||||
gitCommand.OSCommand.Config.GetUserConfig().Set("os.openLinkCommand", "open {{link}}")
|
||||
gitCommand.Config.GetUserConfig().Set("services", map[string]string{
|
||||
// valid configuration for a custom service URL
|
||||
"git.work.com": "gitlab:code.work.com",
|
||||
// invalid configurations for a custom service URL
|
||||
"invalid.work.com": "noservice:invalid.work.com",
|
||||
"noservice.work.com": "noservice.work.com",
|
||||
})
|
||||
dummyPullRequest := NewPullRequest(gitCommand)
|
||||
s.test(dummyPullRequest.Create(s.branch))
|
||||
})
|
||||
|
||||
@@ -1,24 +1,8 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// Remote : A git remote
|
||||
type Remote struct {
|
||||
Name string
|
||||
Urls []string
|
||||
Selected bool
|
||||
Branches []*RemoteBranch
|
||||
}
|
||||
|
||||
// GetDisplayStrings returns the display string of a remote
|
||||
func (r *Remote) GetDisplayStrings(isFocused bool) []string {
|
||||
|
||||
branchCount := len(r.Branches)
|
||||
|
||||
return []string{r.Name, utils.ColoredString(fmt.Sprintf("%d branches", branchCount), color.FgBlue)}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// Remote Branch : A git remote branch
|
||||
type RemoteBranch struct {
|
||||
Name string
|
||||
Selected bool
|
||||
RemoteName string
|
||||
}
|
||||
|
||||
// GetDisplayStrings returns the display string of branch
|
||||
func (b *RemoteBranch) GetDisplayStrings(isFocused bool) []string {
|
||||
displayName := utils.ColoredString(b.Name, GetBranchColor(b.Name))
|
||||
|
||||
return []string{displayName}
|
||||
func (r *RemoteBranch) FullName() string {
|
||||
return r.RemoteName + "/" + r.Name
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package commands
|
||||
|
||||
import "fmt"
|
||||
|
||||
// StashEntry : A git stash entry
|
||||
type StashEntry struct {
|
||||
Index int
|
||||
Name string
|
||||
DisplayString string
|
||||
Index int
|
||||
Name string
|
||||
}
|
||||
|
||||
// GetDisplayStrings returns the display string of branch
|
||||
func (s *StashEntry) GetDisplayStrings(isFocused bool) []string {
|
||||
return []string{s.DisplayString}
|
||||
func (s *StashEntry) RefName() string {
|
||||
return fmt.Sprintf("stash@{%d}", s.Index)
|
||||
}
|
||||
|
||||
@@ -4,8 +4,3 @@ package commands
|
||||
type Tag struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
// GetDisplayStrings returns the display string of a remote
|
||||
func (r *Tag) GetDisplayStrings(isFocused bool) []string {
|
||||
return []string{r.Name}
|
||||
}
|
||||
|
||||
@@ -243,28 +243,44 @@ func GetDefaultConfig() []byte {
|
||||
scrollHeight: 2
|
||||
scrollPastBottom: true
|
||||
mouseEvents: true
|
||||
skipUnstageLineWarning: false
|
||||
skipStashWarning: true
|
||||
sidePanelWidth: 0.3333
|
||||
theme:
|
||||
lightTheme: false
|
||||
activeBorderColor:
|
||||
- white
|
||||
- green
|
||||
- bold
|
||||
inactiveBorderColor:
|
||||
- white
|
||||
optionsTextColor:
|
||||
- blue
|
||||
selectedLineBgColor:
|
||||
- default
|
||||
selectedRangeBgColor:
|
||||
- blue
|
||||
commitLength:
|
||||
show: true
|
||||
git:
|
||||
paging:
|
||||
colorArg: always
|
||||
useConfig: false
|
||||
merging:
|
||||
manualCommit: false
|
||||
args: ""
|
||||
pull:
|
||||
mode: 'merge' # one of 'merge' | 'rebase' | 'ff-only'
|
||||
skipHookPrefix: 'WIP'
|
||||
autoFetch: true
|
||||
branchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --"
|
||||
overrideGpg: false # prevents lazygit from spawning a separate process when using GPG
|
||||
update:
|
||||
method: prompt # can be: prompt | background | never
|
||||
days: 14 # how often a update is checked for
|
||||
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
|
||||
splashUpdatesIndex: 0
|
||||
confirmOnQuit: false
|
||||
quitOnTopLevelReturn: true
|
||||
keybinding:
|
||||
universal:
|
||||
quit: 'q'
|
||||
@@ -276,10 +292,17 @@ keybinding:
|
||||
nextItem: '<down>'
|
||||
prevItem-alt: 'k'
|
||||
nextItem-alt: 'j'
|
||||
prevPage: ','
|
||||
nextPage: '.'
|
||||
gotoTop: '<'
|
||||
gotoBottom: '>'
|
||||
prevBlock: '<left>'
|
||||
nextBlock: '<right>'
|
||||
prevBlock-alt: 'h'
|
||||
nextBlock-alt: 'l'
|
||||
nextMatch: 'n'
|
||||
prevMatch: 'N'
|
||||
startSearch: '/'
|
||||
optionMenu: 'x'
|
||||
optionMenu-alt1: '?'
|
||||
select: '<space>'
|
||||
@@ -294,7 +317,7 @@ keybinding:
|
||||
scrollDownMain-alt1: 'J'
|
||||
scrollUpMain-alt2: '<c-u>'
|
||||
scrollDownMain-alt2: '<c-d>'
|
||||
executeCustomCommand: 'X'
|
||||
executeCustomCommand: ':'
|
||||
createRebaseOptionsMenu: 'm'
|
||||
pushFiles: 'P'
|
||||
pullFiles: 'p'
|
||||
@@ -302,6 +325,13 @@ keybinding:
|
||||
createPatchOptionsMenu: '<c-p>'
|
||||
nextTab: ']'
|
||||
prevTab: '['
|
||||
nextScreenMode: '+'
|
||||
prevScreenMode: '_'
|
||||
undo: 'z'
|
||||
redo: '<c-z>'
|
||||
filteringMenu: <c-s>
|
||||
diffingMenu: '<c-e>'
|
||||
copyToClipboard: '<c-o>'
|
||||
status:
|
||||
checkForUpdate: 'u'
|
||||
recentRepos: '<enter>'
|
||||
@@ -322,6 +352,7 @@ keybinding:
|
||||
checkoutBranchByName: 'c'
|
||||
forceCheckoutBranch: 'F'
|
||||
rebaseBranch: 'r'
|
||||
renameBranch: 'R'
|
||||
mergeIntoCurrentBranch: 'M'
|
||||
viewGitFlowOptions: 'i'
|
||||
fastForward: 'f'
|
||||
@@ -345,8 +376,8 @@ keybinding:
|
||||
cherryPickCopyRange: 'C'
|
||||
pasteCommits: 'v'
|
||||
tagCommit: 'T'
|
||||
toggleDiffCommit: 'i'
|
||||
checkoutCommit: '<space>'
|
||||
resetCherryPick: '<c-R>'
|
||||
stash:
|
||||
popStash: 'g'
|
||||
commitFiles:
|
||||
@@ -356,7 +387,6 @@ keybinding:
|
||||
toggleDragSelect-alt: 'V'
|
||||
toggleSelectHunk: 'a'
|
||||
pickBothHunks: 'b'
|
||||
undo: 'z'
|
||||
`)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,5 +7,6 @@ func GetPlatformDefaultConfig() []byte {
|
||||
return []byte(
|
||||
`os:
|
||||
openCommand: 'open {{filename}}'
|
||||
openLinkCommand: 'open {{link}}'`)
|
||||
openLinkCommand: 'open {{link}}'
|
||||
copyToClipboardCommand: 'bash -c "echo -n {{str}} | pbcopy"'`)
|
||||
}
|
||||
|
||||
@@ -5,5 +5,6 @@ func GetPlatformDefaultConfig() []byte {
|
||||
return []byte(
|
||||
`os:
|
||||
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'
|
||||
openLinkCommand: 'sh -c "xdg-open {{link}} >/dev/null"'`)
|
||||
openLinkCommand: 'sh -c "xdg-open {{link}} >/dev/null"'
|
||||
copyToClipboardCommand: 'bash -c "echo -n {{str}} | xclip -selection clipboard"'`)
|
||||
}
|
||||
|
||||
@@ -5,5 +5,6 @@ func GetPlatformDefaultConfig() []byte {
|
||||
return []byte(
|
||||
`os:
|
||||
openCommand: 'cmd /c "start "" {{filename}}"'
|
||||
openLinkCommand: 'cmd /c "start "" {{link}}"'`)
|
||||
openLinkCommand: 'cmd /c "start "" {{link}}"'
|
||||
copyToClipboardCommand: 'cmd \c "echo -n {{str}} > /dev/clipboard"'`)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
@@ -49,19 +51,28 @@ func (m *statusManager) getStatusString() string {
|
||||
// WithWaitingStatus wraps a function and shows a waiting status while the function is still executing
|
||||
func (gui *Gui) WithWaitingStatus(name string, f func() error) error {
|
||||
go func() {
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
gui.statusManager.addWaitingStatus(name)
|
||||
return nil
|
||||
})
|
||||
gui.statusManager.addWaitingStatus(name)
|
||||
|
||||
defer gui.g.Update(func(g *gocui.Gui) error {
|
||||
defer func() {
|
||||
gui.statusManager.removeStatus(name)
|
||||
return nil
|
||||
})
|
||||
}()
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(time.Millisecond * 50)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
appStatus := gui.statusManager.getStatusString()
|
||||
gui.Log.Warn(appStatus)
|
||||
if appStatus == "" {
|
||||
return
|
||||
}
|
||||
gui.renderString(gui.g, "appStatus", appStatus)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := f(); err != nil {
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -37,83 +37,63 @@ func (gui *Gui) handleBranchSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
|
||||
// This really shouldn't happen: there should always be a master branch
|
||||
if len(gui.State.Branches) == 0 {
|
||||
return gui.renderString(g, "main", gui.Tr.SLocalize("NoBranchesThisRepo"))
|
||||
return gui.newStringTask("main", gui.Tr.SLocalize("NoBranchesThisRepo"))
|
||||
}
|
||||
branch := gui.getSelectedBranch()
|
||||
if err := gui.focusPoint(0, gui.State.Panels.Branches.SelectedLine, len(gui.State.Branches), v); err != nil {
|
||||
return err
|
||||
v.FocusPoint(0, gui.State.Panels.Branches.SelectedLine)
|
||||
|
||||
if gui.inDiffMode() {
|
||||
return gui.renderDiff()
|
||||
}
|
||||
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.GetBranchGraphCmdStr(branch.Name),
|
||||
)
|
||||
if err := gui.newCmdTask("main", cmd); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
go func() {
|
||||
_ = gui.RenderSelectedBranchUpstreamDifferences()
|
||||
}()
|
||||
go func() {
|
||||
upstream, _ := gui.GitCommand.GetUpstreamForBranch(branch.Name)
|
||||
if strings.Contains(upstream, "no upstream configured for branch") || strings.Contains(upstream, "unknown revision or path not in the working tree") {
|
||||
upstream = gui.Tr.SLocalize("notTrackingRemote")
|
||||
}
|
||||
graph, err := gui.GitCommand.GetBranchGraph(branch.Name)
|
||||
if err != nil && strings.HasPrefix(graph, "fatal: ambiguous argument") {
|
||||
graph = gui.Tr.SLocalize("NoTrackingThisBranch")
|
||||
}
|
||||
_ = gui.renderString(g, "main", fmt.Sprintf("%s → %s\n\n%s", utils.ColoredString(branch.Name, color.FgGreen), utils.ColoredString(upstream, color.FgRed), graph))
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) RenderSelectedBranchUpstreamDifferences() error {
|
||||
// here we tell the selected branch that it is selected.
|
||||
// this is necessary for showing stats on a branch that is selected, because
|
||||
// the displaystring function doesn't have access to gui state to tell if it's selected
|
||||
for i, branch := range gui.State.Branches {
|
||||
branch.Selected = i == gui.State.Panels.Branches.SelectedLine
|
||||
}
|
||||
|
||||
branch := gui.getSelectedBranch()
|
||||
branch.Pushables, branch.Pullables = gui.GitCommand.GetBranchUpstreamDifferenceCount(branch.Name)
|
||||
return gui.renderListPanel(gui.getBranchesView(), gui.State.Branches)
|
||||
}
|
||||
|
||||
// gui.refreshStatus is called at the end of this because that's when we can
|
||||
// be sure there is a state.Branches array to pick the current branch from
|
||||
func (gui *Gui) refreshBranches(g *gocui.Gui) error {
|
||||
if err := gui.refreshRemotes(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.refreshTags(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
g.Update(func(g *gocui.Gui) error {
|
||||
builder, err := commands.NewBranchListBuilder(gui.Log, gui.GitCommand)
|
||||
func (gui *Gui) refreshBranches() {
|
||||
reflogCommits := gui.State.FilteredReflogCommits
|
||||
if gui.inFilterMode() {
|
||||
// in filter mode we filter our reflog commits to just those containing the path
|
||||
// however we need all the reflog entries to populate the recencies of our branches
|
||||
// which allows us to order them correctly. So if we're filtering we'll just
|
||||
// manually load all the reflog commits here
|
||||
var err error
|
||||
reflogCommits, _, err = gui.GitCommand.GetReflogCommits(nil, "")
|
||||
if err != nil {
|
||||
return err
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
gui.State.Branches = builder.Build()
|
||||
}
|
||||
|
||||
// TODO: if we're in the remotes view and we've just deleted a remote we need to refresh accordingly
|
||||
if gui.getBranchesView().Context == "local-branches" {
|
||||
gui.refreshSelectedLine(&gui.State.Panels.Branches.SelectedLine, len(gui.State.Branches))
|
||||
if err := gui.RenderSelectedBranchUpstreamDifferences(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
builder, err := commands.NewBranchListBuilder(gui.Log, gui.GitCommand, reflogCommits)
|
||||
if err != nil {
|
||||
_ = gui.surfaceError(err)
|
||||
}
|
||||
gui.State.Branches = builder.Build()
|
||||
|
||||
return gui.refreshStatus(g)
|
||||
})
|
||||
return nil
|
||||
// TODO: if we're in the remotes view and we've just deleted a remote we need to refresh accordingly
|
||||
if gui.getBranchesView().Context == "local-branches" {
|
||||
_ = gui.renderLocalBranchesWithSelection()
|
||||
}
|
||||
|
||||
gui.refreshStatus()
|
||||
}
|
||||
|
||||
func (gui *Gui) renderLocalBranchesWithSelection() error {
|
||||
branchesView := gui.getBranchesView()
|
||||
|
||||
gui.refreshSelectedLine(&gui.State.Panels.Branches.SelectedLine, len(gui.State.Branches))
|
||||
if err := gui.renderListPanel(branchesView, gui.State.Branches); err != nil {
|
||||
return err
|
||||
}
|
||||
if gui.g.CurrentView() == branchesView && branchesView.Context == "local-branches" {
|
||||
displayStrings := presentation.GetBranchListDisplayStrings(gui.State.Branches, gui.State.ScreenMode != SCREEN_NORMAL, gui.State.Diff.Ref)
|
||||
gui.renderDisplayStrings(branchesView, displayStrings)
|
||||
if gui.g.CurrentView() == branchesView {
|
||||
if err := gui.handleBranchSelect(gui.g, branchesView); err != nil {
|
||||
return err
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,10 +107,10 @@ func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error {
|
||||
return nil
|
||||
}
|
||||
if gui.State.Panels.Branches.SelectedLine == 0 {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("AlreadyCheckedOutBranch"))
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("AlreadyCheckedOutBranch"))
|
||||
}
|
||||
branch := gui.getSelectedBranch()
|
||||
return gui.handleCheckoutRef(branch.Name)
|
||||
return gui.handleCheckoutRef(branch.Name, handleCheckoutRefOptions{})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreatePullRequestPress(g *gocui.Gui, v *gocui.View) error {
|
||||
@@ -138,7 +118,7 @@ func (gui *Gui) handleCreatePullRequestPress(g *gocui.Gui, v *gocui.View) error
|
||||
|
||||
branch := gui.getSelectedBranch()
|
||||
if err := pullRequest.Create(branch); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -149,8 +129,9 @@ func (gui *Gui) handleGitFetch(g *gocui.Gui, v *gocui.View) error {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
unamePassOpend, err := gui.fetch(g, v, true)
|
||||
gui.HandleCredentialsPopup(g, unamePassOpend, err)
|
||||
err := gui.fetch(true)
|
||||
gui.handleCredentialsPopup(err)
|
||||
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
@@ -160,55 +141,84 @@ func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
|
||||
message := gui.Tr.SLocalize("SureForceCheckout")
|
||||
title := gui.Tr.SLocalize("ForceCheckoutBranch")
|
||||
return gui.createConfirmationPanel(g, v, true, title, message, func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.Checkout(branch.Name, true); err != nil {
|
||||
gui.createErrorPanel(g, err.Error())
|
||||
if err := gui.GitCommand.Checkout(branch.Name, commands.CheckoutOptions{Force: true}); err != nil {
|
||||
_ = gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(g)
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCheckoutRef(ref string) error {
|
||||
if err := gui.GitCommand.Checkout(ref, false); err != nil {
|
||||
// note, this will only work for english-language git commands. If we force git to use english, and the error isn't this one, then the user will receive an english command they may not understand. I'm not sure what the best solution to this is. Running the command once in english and a second time in the native language is one option
|
||||
type handleCheckoutRefOptions struct {
|
||||
WaitingStatus string
|
||||
EnvVars []string
|
||||
onRefNotFound func(ref string) error
|
||||
}
|
||||
|
||||
if strings.Contains(err.Error(), "Please commit your changes or stash them before you switch branch") {
|
||||
// offer to autostash changes
|
||||
return gui.createConfirmationPanel(gui.g, gui.getBranchesView(), true, gui.Tr.SLocalize("AutoStashTitle"), gui.Tr.SLocalize("AutoStashPrompt"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.StashSave(gui.Tr.SLocalize("StashPrefix") + ref); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
if err := gui.GitCommand.Checkout(ref, false); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
|
||||
// checkout successful so we select the new branch
|
||||
gui.State.Panels.Branches.SelectedLine = 0
|
||||
|
||||
if err := gui.GitCommand.StashDo(0, "pop"); err != nil {
|
||||
if err := gui.refreshSidePanels(g); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
return gui.refreshSidePanels(g)
|
||||
}, nil)
|
||||
}
|
||||
|
||||
if err := gui.createErrorPanel(gui.g, err.Error()); err != nil {
|
||||
return err
|
||||
}
|
||||
func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions) error {
|
||||
waitingStatus := options.WaitingStatus
|
||||
if waitingStatus == "" {
|
||||
waitingStatus = gui.Tr.SLocalize("CheckingOutStatus")
|
||||
}
|
||||
|
||||
gui.State.Panels.Branches.SelectedLine = 0
|
||||
gui.State.Panels.Commits.SelectedLine = 0
|
||||
return gui.refreshSidePanels(gui.g)
|
||||
cmdOptions := commands.CheckoutOptions{Force: false, EnvVars: options.EnvVars}
|
||||
|
||||
onSuccess := func() {
|
||||
gui.State.Panels.Branches.SelectedLine = 0
|
||||
gui.State.Panels.Commits.SelectedLine = 0
|
||||
// loading a heap of commits is slow so we limit them whenever doing a reset
|
||||
gui.State.Panels.Commits.LimitCommits = true
|
||||
}
|
||||
|
||||
return gui.WithWaitingStatus(waitingStatus, func() error {
|
||||
if err := gui.GitCommand.Checkout(ref, cmdOptions); err != nil {
|
||||
// note, this will only work for english-language git commands. If we force git to use english, and the error isn't this one, then the user will receive an english command they may not understand. I'm not sure what the best solution to this is. Running the command once in english and a second time in the native language is one option
|
||||
|
||||
if options.onRefNotFound != nil && strings.Contains(err.Error(), "did not match any file(s) known to git") {
|
||||
return options.onRefNotFound(ref)
|
||||
}
|
||||
|
||||
if strings.Contains(err.Error(), "Please commit your changes or stash them before you switch branch") {
|
||||
// offer to autostash changes
|
||||
return gui.createConfirmationPanel(gui.g, gui.getBranchesView(), true, gui.Tr.SLocalize("AutoStashTitle"), gui.Tr.SLocalize("AutoStashPrompt"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
|
||||
if err := gui.GitCommand.StashSave(gui.Tr.SLocalize("StashPrefix") + ref); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
if err := gui.GitCommand.Checkout(ref, cmdOptions); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
onSuccess()
|
||||
if err := gui.GitCommand.StashDo(0, "pop"); err != nil {
|
||||
if err := gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI}); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI})
|
||||
}, nil)
|
||||
}
|
||||
|
||||
if err := gui.surfaceError(err); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
onSuccess()
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI})
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCheckoutByName(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.createPromptPanel(g, v, gui.Tr.SLocalize("BranchName")+":", "", func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.handleCheckoutRef(gui.trimmedContent(v))
|
||||
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("BranchName")+":", "", func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.handleCheckoutRef(gui.trimmedContent(v), handleCheckoutRefOptions{
|
||||
onRefNotFound: func(ref string) error {
|
||||
return gui.createConfirmationPanel(gui.g, v, true, gui.Tr.SLocalize("BranchNotFoundTitle"), fmt.Sprintf("%s %s%s", gui.Tr.SLocalize("BranchNotFoundPrompt"), ref, "?"), func(_g *gocui.Gui, _v *gocui.View) error {
|
||||
return gui.createNewBranchWithName(ref)
|
||||
}, nil)
|
||||
},
|
||||
})
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) getCheckedOutBranch() *commands.Branch {
|
||||
@@ -220,31 +230,38 @@ func (gui *Gui) getCheckedOutBranch() *commands.Branch {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleNewBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
branch := gui.getCheckedOutBranch()
|
||||
branch := gui.getSelectedBranch()
|
||||
if branch == nil {
|
||||
return nil
|
||||
}
|
||||
message := gui.Tr.TemplateLocalize(
|
||||
"NewBranchNameBranchOff",
|
||||
Teml{
|
||||
"branchName": branch.Name,
|
||||
},
|
||||
)
|
||||
gui.createPromptPanel(g, v, message, "", func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.NewBranch(gui.trimmedContent(v)); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
gui.refreshSidePanels(g)
|
||||
return gui.handleBranchSelect(g, v)
|
||||
return gui.createPromptPanel(g, v, message, "", func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.createNewBranchWithName(gui.trimmedContent(v))
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) createNewBranchWithName(newBranchName string) error {
|
||||
branch := gui.getSelectedBranch()
|
||||
if branch == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := gui.GitCommand.NewBranch(newBranchName, branch.Name); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
gui.State.Panels.Branches.SelectedLine = 0
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleDeleteBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.deleteBranch(g, v, false)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleForceDeleteBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.deleteBranch(g, v, true)
|
||||
}
|
||||
|
||||
func (gui *Gui) deleteBranch(g *gocui.Gui, v *gocui.View, force bool) error {
|
||||
selectedBranch := gui.getSelectedBranch()
|
||||
if selectedBranch == nil {
|
||||
@@ -252,7 +269,7 @@ func (gui *Gui) deleteBranch(g *gocui.Gui, v *gocui.View, force bool) error {
|
||||
}
|
||||
checkedOutBranch := gui.getCheckedOutBranch()
|
||||
if checkedOutBranch.Name == selectedBranch.Name {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantDeleteCheckOutBranch"))
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("CantDeleteCheckOutBranch"))
|
||||
}
|
||||
return gui.deleteNamedBranch(g, v, selectedBranch, force)
|
||||
}
|
||||
@@ -277,19 +294,23 @@ func (gui *Gui) deleteNamedBranch(g *gocui.Gui, v *gocui.View, selectedBranch *c
|
||||
if !force && strings.Contains(errMessage, "is not fully merged") {
|
||||
return gui.deleteNamedBranch(g, v, selectedBranch, true)
|
||||
}
|
||||
return gui.createErrorPanel(g, errMessage)
|
||||
return gui.createErrorPanel(errMessage)
|
||||
}
|
||||
return gui.refreshSidePanels(g)
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{BRANCHES}})
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
if gui.GitCommand.IsHeadDetached() {
|
||||
return gui.createErrorPanel(gui.g, "Cannot merge branch in detached head state. You might have checked out a commit directly or a remote branch, in which case you should checkout the local branch you want to be on")
|
||||
return gui.createErrorPanel("Cannot merge branch in detached head state. You might have checked out a commit directly or a remote branch, in which case you should checkout the local branch you want to be on")
|
||||
}
|
||||
checkedOutBranchName := gui.getCheckedOutBranch().Name
|
||||
if checkedOutBranchName == branchName {
|
||||
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("CantMergeBranchIntoItself"))
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("CantMergeBranchIntoItself"))
|
||||
}
|
||||
prompt := gui.Tr.TemplateLocalize(
|
||||
"ConfirmMerge",
|
||||
@@ -301,12 +322,16 @@ func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error {
|
||||
return gui.createConfirmationPanel(gui.g, gui.getBranchesView(), true, gui.Tr.SLocalize("MergingTitle"), prompt,
|
||||
func(g *gocui.Gui, v *gocui.View) error {
|
||||
|
||||
err := gui.GitCommand.Merge(branchName)
|
||||
err := gui.GitCommand.Merge(branchName, commands.MergeOpts{})
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
selectedBranchName := gui.getSelectedBranch().Name
|
||||
return gui.mergeBranchIntoCheckedOutBranch(selectedBranchName)
|
||||
}
|
||||
@@ -317,9 +342,13 @@ func (gui *Gui) handleRebaseOntoLocalBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRebaseOntoBranch(selectedBranchName string) error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
checkedOutBranch := gui.getCheckedOutBranch().Name
|
||||
if selectedBranchName == checkedOutBranch {
|
||||
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("CantRebaseOntoSelf"))
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("CantRebaseOntoSelf"))
|
||||
}
|
||||
prompt := gui.Tr.TemplateLocalize(
|
||||
"ConfirmRebase",
|
||||
@@ -344,15 +373,15 @@ func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
|
||||
return nil
|
||||
}
|
||||
if branch.Pushables == "?" {
|
||||
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("FwdNoUpstream"))
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("FwdNoUpstream"))
|
||||
}
|
||||
if branch.Pushables != "0" {
|
||||
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("FwdCommitsToPush"))
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("FwdCommitsToPush"))
|
||||
}
|
||||
|
||||
upstream, err := gui.GitCommand.GetUpstreamForBranch(branch.Name)
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
split := strings.Split(upstream, "/")
|
||||
@@ -369,11 +398,12 @@ func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
|
||||
go func() {
|
||||
_ = gui.createLoaderPanel(gui.g, v, message)
|
||||
|
||||
if err := gui.GitCommand.FastForward(branch.Name, remoteName, remoteBranchName); err != nil {
|
||||
_ = gui.createErrorPanel(gui.g, err.Error())
|
||||
if gui.State.Panels.Branches.SelectedLine == 0 {
|
||||
_ = gui.pullWithMode("ff-only", PullFilesOptions{})
|
||||
} else {
|
||||
_ = gui.closeConfirmationPrompt(gui.g, true)
|
||||
_ = gui.RenderSelectedBranchUpstreamDifferences()
|
||||
err := gui.GitCommand.FastForward(branch.Name, remoteName, remoteBranchName, gui.promptUserForCredential)
|
||||
gui.handleCredentialsPopup(err)
|
||||
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{BRANCHES}})
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
@@ -390,6 +420,9 @@ func (gui *Gui) onBranchesTabClick(tabIndex int) error {
|
||||
func (gui *Gui) switchBranchesPanelContext(context string) error {
|
||||
branchesView := gui.getBranchesView()
|
||||
branchesView.Context = context
|
||||
if err := gui.onSearchEscape(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
contextTabIndexMap := map[string]int{
|
||||
"local-branches": 0,
|
||||
@@ -400,7 +433,13 @@ func (gui *Gui) switchBranchesPanelContext(context string) error {
|
||||
|
||||
branchesView.TabIndex = contextTabIndexMap[context]
|
||||
|
||||
switch context {
|
||||
return gui.refreshBranchesViewWithSelection()
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshBranchesViewWithSelection() error {
|
||||
branchesView := gui.getBranchesView()
|
||||
|
||||
switch branchesView.Context {
|
||||
case "local-branches":
|
||||
return gui.renderLocalBranchesWithSelection()
|
||||
case "remotes":
|
||||
@@ -425,3 +464,81 @@ func (gui *Gui) handlePrevBranchesTab(g *gocui.Gui, v *gocui.View) error {
|
||||
utils.ModuloWithWrap(v.TabIndex-1, len(v.Tabs)),
|
||||
)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateResetToBranchMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
branch := gui.getSelectedBranch()
|
||||
if branch == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.createResetMenu(branch.Name)
|
||||
}
|
||||
|
||||
func (gui *Gui) onBranchesPanelSearchSelect(selectedLine int) error {
|
||||
branchesView := gui.getBranchesView()
|
||||
switch branchesView.Context {
|
||||
case "local-branches":
|
||||
gui.State.Panels.Branches.SelectedLine = selectedLine
|
||||
return gui.handleBranchSelect(gui.g, branchesView)
|
||||
case "remotes":
|
||||
gui.State.Panels.Remotes.SelectedLine = selectedLine
|
||||
return gui.handleRemoteSelect(gui.g, branchesView)
|
||||
case "remote-branches":
|
||||
gui.State.Panels.RemoteBranches.SelectedLine = selectedLine
|
||||
return gui.handleRemoteBranchSelect(gui.g, branchesView)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRenameBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
branch := gui.getSelectedBranch()
|
||||
if branch == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: find a way to not checkout the branch here if it's not the current branch (i.e. find some
|
||||
// way to get it to show up in the reflog)
|
||||
|
||||
promptForNewName := func() error {
|
||||
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("NewBranchNamePrompt")+" "+branch.Name+":", "", func(g *gocui.Gui, v *gocui.View) error {
|
||||
newName := gui.trimmedContent(v)
|
||||
if err := gui.GitCommand.RenameBranch(branch.Name, newName); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
// need to checkout so that the branch shows up in our reflog and therefore
|
||||
// doesn't get lost among all the other branches when we switch to something else
|
||||
if err := gui.GitCommand.Checkout(newName, commands.CheckoutOptions{Force: false}); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
})
|
||||
}
|
||||
|
||||
// I could do an explicit check here for whether the branch is tracking a remote branch
|
||||
// but if we've selected it we'll already know that via Pullables and Pullables.
|
||||
// Bit of a hack but I'm lazy.
|
||||
notTrackingRemote := branch.Pullables == "?"
|
||||
if notTrackingRemote {
|
||||
return promptForNewName()
|
||||
}
|
||||
return gui.createConfirmationPanel(gui.g, v, true, gui.Tr.SLocalize("renameBranch"), gui.Tr.SLocalize("RenameBranchWarning"), func(_g *gocui.Gui, _v *gocui.View) error {
|
||||
return promptForNewName()
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) currentBranch() *commands.Branch {
|
||||
if len(gui.State.Branches) == 0 {
|
||||
return nil
|
||||
}
|
||||
return gui.State.Branches[0]
|
||||
}
|
||||
|
||||
func (gui *Gui) handleClipboardCopyBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
branch := gui.getSelectedBranch()
|
||||
if branch == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.OSCommand.CopyToClipboard(branch.Name)
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ import (
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
)
|
||||
|
||||
func (gui *Gui) getSelectedCommitFile(g *gocui.Gui) *commands.CommitFile {
|
||||
func (gui *Gui) getSelectedCommitFile() *commands.CommitFile {
|
||||
selectedLine := gui.State.Panels.CommitFiles.SelectedLine
|
||||
if selectedLine == -1 {
|
||||
return nil
|
||||
@@ -29,25 +30,30 @@ func (gui *Gui) handleCommitFileSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
gui.getMainView().Title = "Patch"
|
||||
gui.State.Panels.LineByLine = nil
|
||||
if gui.currentViewName() == "commitFiles" {
|
||||
gui.handleEscapeLineByLinePanel()
|
||||
}
|
||||
|
||||
commitFile := gui.getSelectedCommitFile(g)
|
||||
commitFile := gui.getSelectedCommitFile()
|
||||
if commitFile == nil {
|
||||
return gui.renderString(g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
|
||||
gui.renderString(g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := gui.refreshSecondaryPatchPanel(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.focusPoint(0, gui.State.Panels.CommitFiles.SelectedLine, len(gui.State.CommitFiles), v); err != nil {
|
||||
return err
|
||||
v.FocusPoint(0, gui.State.Panels.CommitFiles.SelectedLine)
|
||||
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.ShowCommitFileCmdStr(commitFile.Sha, commitFile.Name, false),
|
||||
)
|
||||
if err := gui.newPtyTask("main", cmd); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
commitText, err := gui.GitCommand.ShowCommitFile(commitFile.Sha, commitFile.Name, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.renderString(g, "main", commitText)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSwitchToCommitsPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
@@ -58,10 +64,10 @@ func (gui *Gui) handleCheckoutCommitFile(g *gocui.Gui, v *gocui.View) error {
|
||||
file := gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLine]
|
||||
|
||||
if err := gui.GitCommand.CheckoutFile(file.Sha, file.Name); err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.refreshFiles()
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleDiscardOldFileChange(g *gocui.Gui, v *gocui.View) error {
|
||||
@@ -79,7 +85,7 @@ func (gui *Gui) handleDiscardOldFileChange(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(gui.g)
|
||||
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI})
|
||||
})
|
||||
}, nil)
|
||||
}
|
||||
@@ -93,39 +99,53 @@ func (gui *Gui) refreshCommitFilesView() error {
|
||||
return err
|
||||
}
|
||||
|
||||
commit := gui.getSelectedCommit(gui.g)
|
||||
commit := gui.getSelectedCommit()
|
||||
if commit == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
files, err := gui.GitCommand.GetCommitFiles(commit.Sha, gui.GitCommand.PatchManager)
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
gui.State.CommitFiles = files
|
||||
|
||||
gui.refreshSelectedLine(&gui.State.Panels.CommitFiles.SelectedLine, len(gui.State.CommitFiles))
|
||||
|
||||
if err := gui.renderListPanel(gui.getCommitFilesView(), gui.State.CommitFiles); err != nil {
|
||||
return err
|
||||
}
|
||||
commitsFileView := gui.getCommitFilesView()
|
||||
displayStrings := presentation.GetCommitFileListDisplayStrings(gui.State.CommitFiles, gui.State.Diff.Ref)
|
||||
gui.renderDisplayStrings(commitsFileView, displayStrings)
|
||||
|
||||
return gui.handleCommitFileSelect(gui.g, gui.getCommitFilesView())
|
||||
return gui.handleCommitFileSelect(gui.g, commitsFileView)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleOpenOldCommitFile(g *gocui.Gui, v *gocui.View) error {
|
||||
file := gui.getSelectedCommitFile(g)
|
||||
file := gui.getSelectedCommitFile()
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.openFile(file.Name)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleEditCommitFile(g *gocui.Gui, v *gocui.View) error {
|
||||
file := gui.getSelectedCommitFile()
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.editFile(file.Name)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleToggleFileForPatch(g *gocui.Gui, v *gocui.View) error {
|
||||
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
commitFile := gui.getSelectedCommitFile(g)
|
||||
commitFile := gui.getSelectedCommitFile()
|
||||
if commitFile == nil {
|
||||
return gui.renderString(g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
|
||||
gui.renderString(g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
|
||||
return nil
|
||||
}
|
||||
|
||||
toggleTheFile := func() error {
|
||||
@@ -160,7 +180,7 @@ func (gui *Gui) startPatchManager() error {
|
||||
diffMap[commitFile.Name] = commitText
|
||||
}
|
||||
|
||||
commit := gui.getSelectedCommit(gui.g)
|
||||
commit := gui.getSelectedCommit()
|
||||
if commit == nil {
|
||||
return errors.New("No commit selected")
|
||||
}
|
||||
@@ -178,9 +198,10 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
|
||||
return err
|
||||
}
|
||||
|
||||
commitFile := gui.getSelectedCommitFile(gui.g)
|
||||
commitFile := gui.getSelectedCommitFile()
|
||||
if commitFile == nil {
|
||||
return gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
|
||||
gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
|
||||
return nil
|
||||
}
|
||||
|
||||
enterTheFile := func(selectedLineIdx int) error {
|
||||
@@ -190,9 +211,7 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := gui.changeMainViewsContext("patch-building"); err != nil {
|
||||
return err
|
||||
}
|
||||
gui.changeMainViewsContext("patch-building")
|
||||
if err := gui.switchFocus(gui.g, gui.getCommitFilesView(), gui.getMainView()); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -203,8 +222,15 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
|
||||
return gui.createConfirmationPanel(gui.g, gui.getCommitFilesView(), false, gui.Tr.SLocalize("DiscardPatch"), gui.Tr.SLocalize("DiscardPatchConfirm"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.GitCommand.PatchManager.Reset()
|
||||
return enterTheFile(selectedLineIdx)
|
||||
}, nil)
|
||||
}, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.switchFocus(gui.g, nil, gui.getCommitFilesView())
|
||||
})
|
||||
}
|
||||
|
||||
return enterTheFile(selectedLineIdx)
|
||||
}
|
||||
|
||||
func (gui *Gui) onCommitFilesPanelSearchSelect(selectedLine int) error {
|
||||
gui.State.Panels.CommitFiles.SelectedLine = selectedLine
|
||||
return gui.handleCommitFileSelect(gui.g, gui.getCommitFilesView())
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
func (gui *Gui) runSyncOrAsyncCommand(sub *exec.Cmd, err error) (bool, error) {
|
||||
if err != nil {
|
||||
if err != gui.Errors.ErrSubProcess {
|
||||
return false, gui.createErrorPanel(gui.g, err.Error())
|
||||
return false, gui.surfaceError(err)
|
||||
}
|
||||
}
|
||||
if sub != nil {
|
||||
@@ -28,7 +28,7 @@ func (gui *Gui) runSyncOrAsyncCommand(sub *exec.Cmd, err error) (bool, error) {
|
||||
func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
|
||||
message := gui.trimmedContent(v)
|
||||
if message == "" {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("CommitWithoutMessageErr"))
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("CommitWithoutMessageErr"))
|
||||
}
|
||||
flags := ""
|
||||
skipHookPrefix := gui.Config.GetUserConfig().GetString("git.skipHookPrefix")
|
||||
@@ -48,11 +48,11 @@ func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
|
||||
_ = v.SetOrigin(0, 0)
|
||||
_, _ = g.SetViewOnBottom("commitMessage")
|
||||
_ = gui.switchFocus(g, v, gui.getFilesView())
|
||||
return gui.refreshSidePanels(g)
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitClose(g *gocui.Gui, v *gocui.View) error {
|
||||
g.SetViewOnBottom("commitMessage")
|
||||
_, _ = g.SetViewOnBottom("commitMessage")
|
||||
return gui.switchFocus(g, v, gui.getFilesView())
|
||||
}
|
||||
|
||||
@@ -62,13 +62,15 @@ func (gui *Gui) handleCommitFocused(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
message := gui.Tr.TemplateLocalize(
|
||||
"CloseConfirm",
|
||||
"CommitMessageConfirm",
|
||||
Teml{
|
||||
"keyBindClose": "esc",
|
||||
"keyBindConfirm": "enter",
|
||||
"keyBindNewLine": "tab",
|
||||
},
|
||||
)
|
||||
return gui.renderString(g, "options", message)
|
||||
gui.renderString(g, "options", message)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) getBufferLength(view *gocui.View) string {
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/go-errors/errors"
|
||||
"sync"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// list panel functions
|
||||
|
||||
func (gui *Gui) getSelectedCommit(g *gocui.Gui) *commands.Commit {
|
||||
func (gui *Gui) getSelectedCommit() *commands.Commit {
|
||||
selectedLine := gui.State.Panels.Commits.SelectedLine
|
||||
if selectedLine == -1 {
|
||||
return nil
|
||||
@@ -37,91 +35,116 @@ func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
return err
|
||||
}
|
||||
|
||||
state := gui.State.Panels.Commits
|
||||
if state.SelectedLine > 290 && state.LimitCommits {
|
||||
state.LimitCommits = false
|
||||
go func() {
|
||||
if err := gui.refreshCommitsWithLimit(); err != nil {
|
||||
_ = gui.surfaceError(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
gui.getMainView().Title = "Patch"
|
||||
gui.getSecondaryView().Title = "Custom Patch"
|
||||
gui.State.Panels.LineByLine = nil
|
||||
gui.handleEscapeLineByLinePanel()
|
||||
|
||||
commit := gui.getSelectedCommit(g)
|
||||
commit := gui.getSelectedCommit()
|
||||
if commit == nil {
|
||||
return gui.renderString(g, "main", gui.Tr.SLocalize("NoCommitsThisBranch"))
|
||||
return gui.newStringTask("main", gui.Tr.SLocalize("NoCommitsThisBranch"))
|
||||
}
|
||||
|
||||
if err := gui.focusPoint(0, gui.State.Panels.Commits.SelectedLine, len(gui.State.Commits), v); err != nil {
|
||||
return err
|
||||
v.FocusPoint(0, gui.State.Panels.Commits.SelectedLine)
|
||||
|
||||
if gui.inDiffMode() {
|
||||
return gui.renderDiff()
|
||||
}
|
||||
|
||||
// if specific diff mode is on, don't show diff
|
||||
if gui.State.Panels.Commits.SpecificDiffMode {
|
||||
return nil
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.FilterPath),
|
||||
)
|
||||
if err := gui.newPtyTask("main", cmd); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
|
||||
commitText, err := gui.GitCommand.Show(commit.Sha)
|
||||
return nil
|
||||
}
|
||||
|
||||
// during startup, the bottleneck is fetching the reflog entries. We need these
|
||||
// on startup to sort the branches by recency. So we have two phases: INITIAL, and COMPLETE.
|
||||
// In the initial phase we don't get any reflog commits, but we asynchronously get them
|
||||
// and refresh the branches after that
|
||||
func (gui *Gui) refreshReflogCommitsConsideringStartup() {
|
||||
switch gui.State.StartupStage {
|
||||
case INITIAL:
|
||||
go func() {
|
||||
_ = gui.refreshReflogCommits()
|
||||
gui.refreshBranches()
|
||||
gui.State.StartupStage = COMPLETE
|
||||
}()
|
||||
|
||||
case COMPLETE:
|
||||
_ = gui.refreshReflogCommits()
|
||||
}
|
||||
}
|
||||
|
||||
// whenever we change commits, we should update branches because the upstream/downstream
|
||||
// counts can change. Whenever we change branches we should probably also change commits
|
||||
// e.g. in the case of switching branches.
|
||||
func (gui *Gui) refreshCommits() error {
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
|
||||
go func() {
|
||||
gui.refreshReflogCommitsConsideringStartup()
|
||||
|
||||
gui.refreshBranches()
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
_ = gui.refreshCommitsWithLimit()
|
||||
if gui.g.CurrentView() == gui.getCommitFilesView() || (gui.g.CurrentView() == gui.getMainView() && gui.State.MainContext == "patch-building") {
|
||||
_ = gui.refreshCommitFilesView()
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshCommitsWithLimit() error {
|
||||
builder, err := commands.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr, gui.State.CherryPickedCommits)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.renderString(g, "main", commitText)
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshCommits(g *gocui.Gui) error {
|
||||
g.Update(func(*gocui.Gui) error {
|
||||
builder, err := commands.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr, gui.State.CherryPickedCommits, gui.State.DiffEntries)
|
||||
if err != nil {
|
||||
commits, err := builder.GetCommits(commands.GetCommitsOptions{Limit: gui.State.Panels.Commits.LimitCommits, FilterPath: gui.State.FilterPath})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gui.State.Commits = commits
|
||||
|
||||
if gui.getCommitsView().Context == "branch-commits" {
|
||||
if err := gui.renderBranchCommitsWithSelection(); err != nil {
|
||||
return err
|
||||
}
|
||||
commits, err := builder.GetCommits()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gui.State.Commits = commits
|
||||
}
|
||||
|
||||
// doing this async because it shouldn't hold anything up
|
||||
go func() {
|
||||
if err := gui.refreshReflogCommits(); err != nil {
|
||||
_ = gui.createErrorPanel(gui.g, err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
gui.refreshStatus(g)
|
||||
if gui.getCommitsView().Context == "branch-commits" {
|
||||
if err := gui.renderBranchCommitsWithSelection(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if g.CurrentView() == gui.getCommitFilesView() || (g.CurrentView() == gui.getMainView() || gui.State.MainContext == "patch-building") {
|
||||
return gui.refreshCommitFilesView()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// specific functions
|
||||
|
||||
func (gui *Gui) handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error {
|
||||
return gui.createConfirmationPanel(g, commitView, true, gui.Tr.SLocalize("ResetToCommit"), gui.Tr.SLocalize("SureResetThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
commit := gui.getSelectedCommit(g)
|
||||
if commit == nil {
|
||||
panic(errors.New(gui.Tr.SLocalize("NoCommitsThisBranch")))
|
||||
}
|
||||
|
||||
if err := gui.GitCommand.ResetToCommit(commit.Sha, "mixed"); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
if err := gui.refreshCommits(g); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := gui.refreshFiles(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
gui.resetOrigin(commitView)
|
||||
gui.State.Panels.Commits.SelectedLine = 0
|
||||
return gui.handleCommitSelect(g, commitView)
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(gui.State.Commits) <= 1 {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash"))
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("YouNoCommitsToSquash"))
|
||||
}
|
||||
|
||||
applied, err := gui.handleMidRebaseCommand("squash")
|
||||
@@ -132,28 +155,21 @@ func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("Squash"), gui.Tr.SLocalize("SureSquashThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("Squash"), gui.Tr.SLocalize("SureSquashThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.SLocalize("SquashingStatus"), func() error {
|
||||
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "squash")
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
}, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: move to files panel
|
||||
func (gui *Gui) anyUnStagedChanges(files []*commands.File) bool {
|
||||
for _, file := range files {
|
||||
if file.Tracked && file.HasUnstagedChanges {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(gui.State.Commits) <= 1 {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash"))
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("YouNoCommitsToSquash"))
|
||||
}
|
||||
|
||||
applied, err := gui.handleMidRebaseCommand("fixup")
|
||||
@@ -164,16 +180,19 @@ func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("Fixup"), gui.Tr.SLocalize("SureFixupThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("Fixup"), gui.Tr.SLocalize("SureFixupThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.SLocalize("FixingStatus"), func() error {
|
||||
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "fixup")
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
}, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
applied, err := gui.handleMidRebaseCommand("reword")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -183,20 +202,22 @@ func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
if gui.State.Panels.Commits.SelectedLine != 0 {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlyRenameTopCommit"))
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("OnlyRenameTopCommit"))
|
||||
}
|
||||
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("renameCommit"), "", func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.RenameCommit(v.Buffer()); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
if err := gui.refreshCommits(g); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return gui.handleCommitSelect(g, v)
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
applied, err := gui.handleMidRebaseCommand("reword")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -207,7 +228,7 @@ func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
|
||||
|
||||
subProcess, err := gui.GitCommand.RewordCommit(gui.State.Commits, gui.State.Panels.Commits.SelectedLine)
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
if subProcess != nil {
|
||||
gui.SubProcess = subProcess
|
||||
@@ -231,31 +252,22 @@ func (gui *Gui) handleMidRebaseCommand(action string) (bool, error) {
|
||||
// our input or we set a lazygit client as the EDITOR env variable and have it
|
||||
// request us to edit the commit message when prompted.
|
||||
if action == "reword" {
|
||||
return true, gui.createErrorPanel(gui.g, gui.Tr.SLocalize("rewordNotSupported"))
|
||||
return true, gui.createErrorPanel(gui.Tr.SLocalize("rewordNotSupported"))
|
||||
}
|
||||
|
||||
if err := gui.GitCommand.EditRebaseTodo(gui.State.Panels.Commits.SelectedLine, action); err != nil {
|
||||
return false, gui.createErrorPanel(gui.g, err.Error())
|
||||
return false, gui.surfaceError(err)
|
||||
}
|
||||
return true, gui.refreshCommits(gui.g)
|
||||
}
|
||||
|
||||
// handleMoveTodoDown like handleMidRebaseCommand but for moving an item up in the todo list
|
||||
func (gui *Gui) handleMoveTodoDown(index int) (bool, error) {
|
||||
selectedCommit := gui.State.Commits[index]
|
||||
if selectedCommit.Status != "rebasing" {
|
||||
return false, nil
|
||||
}
|
||||
if gui.State.Commits[index+1].Status != "rebasing" {
|
||||
return true, nil
|
||||
}
|
||||
if err := gui.GitCommand.MoveTodoDown(index); err != nil {
|
||||
return true, gui.createErrorPanel(gui.g, err.Error())
|
||||
}
|
||||
return true, gui.refreshCommits(gui.g)
|
||||
// TODO: consider doing this in a way that is less expensive. We don't actually
|
||||
// need to reload all the commits, just the TODO commits.
|
||||
return true, gui.refreshSidePanels(refreshOptions{scope: []int{COMMITS}})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitDelete(g *gocui.Gui, v *gocui.View) error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
applied, err := gui.handleMidRebaseCommand("drop")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -273,6 +285,10 @@ func (gui *Gui) handleCommitDelete(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitMoveDown(g *gocui.Gui, v *gocui.View) error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
index := gui.State.Panels.Commits.SelectedLine
|
||||
selectedCommit := gui.State.Commits[index]
|
||||
if selectedCommit.Status == "rebasing" {
|
||||
@@ -280,10 +296,10 @@ func (gui *Gui) handleCommitMoveDown(g *gocui.Gui, v *gocui.View) error {
|
||||
return nil
|
||||
}
|
||||
if err := gui.GitCommand.MoveTodoDown(index); err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
gui.State.Panels.Commits.SelectedLine++
|
||||
return gui.refreshCommits(gui.g)
|
||||
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI, scope: []int{COMMITS, BRANCHES}})
|
||||
}
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.SLocalize("MovingStatus"), func() error {
|
||||
@@ -296,6 +312,10 @@ func (gui *Gui) handleCommitMoveDown(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitMoveUp(g *gocui.Gui, v *gocui.View) error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
index := gui.State.Panels.Commits.SelectedLine
|
||||
if index == 0 {
|
||||
return nil
|
||||
@@ -303,10 +323,10 @@ func (gui *Gui) handleCommitMoveUp(g *gocui.Gui, v *gocui.View) error {
|
||||
selectedCommit := gui.State.Commits[index]
|
||||
if selectedCommit.Status == "rebasing" {
|
||||
if err := gui.GitCommand.MoveTodoDown(index - 1); err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
gui.State.Panels.Commits.SelectedLine--
|
||||
return gui.refreshCommits(gui.g)
|
||||
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI, scope: []int{COMMITS, BRANCHES}})
|
||||
}
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.SLocalize("MovingStatus"), func() error {
|
||||
@@ -319,6 +339,10 @@ func (gui *Gui) handleCommitMoveUp(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitEdit(g *gocui.Gui, v *gocui.View) error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
applied, err := gui.handleMidRebaseCommand("edit")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -334,6 +358,10 @@ func (gui *Gui) handleCommitEdit(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitAmendTo(g *gocui.Gui, v *gocui.View) error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.createConfirmationPanel(gui.g, v, true, gui.Tr.SLocalize("AmendCommitTitle"), gui.Tr.SLocalize("AmendCommitPrompt"), func(*gocui.Gui, *gocui.View) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.SLocalize("AmendingStatus"), func() error {
|
||||
err := gui.GitCommand.AmendTo(gui.State.Commits[gui.State.Panels.Commits.SelectedLine].Sha)
|
||||
@@ -343,6 +371,10 @@ func (gui *Gui) handleCommitAmendTo(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitPick(g *gocui.Gui, v *gocui.View) error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
applied, err := gui.handleMidRebaseCommand("pick")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -357,14 +389,22 @@ func (gui *Gui) handleCommitPick(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitRevert(g *gocui.Gui, v *gocui.View) error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.GitCommand.Revert(gui.State.Commits[gui.State.Panels.Commits.SelectedLine].Sha); err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
gui.State.Panels.Commits.SelectedLine++
|
||||
return gui.refreshCommits(gui.g)
|
||||
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI, scope: []int{COMMITS, BRANCHES}})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCopyCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
// get currently selected commit, add the sha to state.
|
||||
commit := gui.State.Commits[gui.State.Panels.Commits.SelectedLine]
|
||||
|
||||
@@ -372,22 +412,29 @@ func (gui *Gui) handleCopyCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
for index, cherryPickedCommit := range gui.State.CherryPickedCommits {
|
||||
if commit.Sha == cherryPickedCommit.Sha {
|
||||
gui.State.CherryPickedCommits = append(gui.State.CherryPickedCommits[0:index], gui.State.CherryPickedCommits[index+1:]...)
|
||||
return gui.refreshCommits(gui.g)
|
||||
return gui.renderBranchCommitsWithSelection()
|
||||
}
|
||||
}
|
||||
|
||||
gui.addCommitToCherryPickedCommits(gui.State.Panels.Commits.SelectedLine)
|
||||
return gui.refreshCommits(gui.g)
|
||||
return gui.renderBranchCommitsWithSelection()
|
||||
}
|
||||
|
||||
func (gui *Gui) cherryPickedCommitShaMap() map[string]bool {
|
||||
commitShaMap := map[string]bool{}
|
||||
for _, commit := range gui.State.CherryPickedCommits {
|
||||
commitShaMap[commit.Sha] = true
|
||||
}
|
||||
return commitShaMap
|
||||
}
|
||||
|
||||
func (gui *Gui) addCommitToCherryPickedCommits(index int) {
|
||||
// not super happy with modifying the state of the Commits array here
|
||||
// but the alternative would be very tricky
|
||||
gui.State.Commits[index].Copied = true
|
||||
commitShaMap := gui.cherryPickedCommitShaMap()
|
||||
commitShaMap[gui.State.Commits[index].Sha] = true
|
||||
|
||||
newCommits := []*commands.Commit{}
|
||||
for _, commit := range gui.State.Commits {
|
||||
if commit.Copied {
|
||||
if commitShaMap[commit.Sha] {
|
||||
// duplicating just the things we need to put in the rebase TODO list
|
||||
newCommits = append(newCommits, &commands.Commit{Name: commit.Name, Sha: commit.Sha})
|
||||
}
|
||||
@@ -397,13 +444,17 @@ func (gui *Gui) addCommitToCherryPickedCommits(index int) {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCopyCommitRange(g *gocui.Gui, v *gocui.View) error {
|
||||
// whenever I add a commit, I need to make sure I retain its order
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
commitShaMap := gui.cherryPickedCommitShaMap()
|
||||
|
||||
// find the last commit that is copied that's above our position
|
||||
// if there are none, startIndex = 0
|
||||
startIndex := 0
|
||||
for index, commit := range gui.State.Commits[0:gui.State.Panels.Commits.SelectedLine] {
|
||||
if commit.Copied {
|
||||
if commitShaMap[commit.Sha] {
|
||||
startIndex = index
|
||||
}
|
||||
}
|
||||
@@ -414,11 +465,15 @@ func (gui *Gui) handleCopyCommitRange(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.addCommitToCherryPickedCommits(index)
|
||||
}
|
||||
|
||||
return gui.refreshCommits(gui.g)
|
||||
return gui.renderBranchCommitsWithSelection()
|
||||
}
|
||||
|
||||
// HandlePasteCommits begins a cherry-pick rebase with the commits the user has copied
|
||||
func (gui *Gui) HandlePasteCommits(g *gocui.Gui, v *gocui.View) error {
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("CherryPick"), gui.Tr.SLocalize("SureCherryPick"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.SLocalize("CherryPickingStatus"), func() error {
|
||||
err := gui.GitCommand.CherryPickCommits(gui.State.CherryPickedCommits)
|
||||
@@ -435,54 +490,6 @@ func (gui *Gui) handleSwitchToCommitFilesPanel(g *gocui.Gui, v *gocui.View) erro
|
||||
return gui.switchFocus(g, gui.getCommitsView(), gui.getCommitFilesView())
|
||||
}
|
||||
|
||||
func (gui *Gui) handleToggleDiffCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
selectLimit := 2
|
||||
|
||||
// get selected commit
|
||||
commit := gui.getSelectedCommit(g)
|
||||
if commit == nil {
|
||||
return gui.renderString(g, "main", gui.Tr.SLocalize("NoCommitsThisBranch"))
|
||||
}
|
||||
|
||||
// if already selected commit delete
|
||||
if idx, has := gui.hasCommit(gui.State.DiffEntries, commit.Sha); has {
|
||||
gui.State.DiffEntries = gui.unchooseCommit(gui.State.DiffEntries, idx)
|
||||
} else {
|
||||
if len(gui.State.DiffEntries) == selectLimit {
|
||||
gui.State.DiffEntries = gui.unchooseCommit(gui.State.DiffEntries, 0)
|
||||
}
|
||||
gui.State.DiffEntries = append(gui.State.DiffEntries, commit)
|
||||
}
|
||||
|
||||
gui.setDiffMode()
|
||||
|
||||
// if selected two commits, display diff between
|
||||
if len(gui.State.DiffEntries) == selectLimit {
|
||||
commitText, err := gui.GitCommand.DiffCommits(gui.State.DiffEntries[0].Sha, gui.State.DiffEntries[1].Sha)
|
||||
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
}
|
||||
|
||||
return gui.renderString(g, "main", commitText)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) setDiffMode() {
|
||||
v := gui.getCommitsView()
|
||||
if len(gui.State.DiffEntries) != 0 {
|
||||
gui.State.Panels.Commits.SpecificDiffMode = true
|
||||
v.Title = gui.Tr.SLocalize("CommitsDiffTitle")
|
||||
} else {
|
||||
gui.State.Panels.Commits.SpecificDiffMode = false
|
||||
v.Title = gui.Tr.SLocalize("CommitsTitle")
|
||||
}
|
||||
|
||||
gui.refreshCommits(gui.g)
|
||||
}
|
||||
|
||||
func (gui *Gui) hasCommit(commits []*commands.Commit, target string) (int, bool) {
|
||||
for idx, commit := range commits {
|
||||
if commit.Sha == target {
|
||||
@@ -497,7 +504,11 @@ func (gui *Gui) unchooseCommit(commits []*commands.Commit, i int) []*commands.Co
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateFixupCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
commit := gui.getSelectedCommit(g)
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
commit := gui.getSelectedCommit()
|
||||
if commit == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -509,15 +520,19 @@ func (gui *Gui) handleCreateFixupCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
},
|
||||
), func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.CreateFixupCommit(commit.Sha); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(gui.g)
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSquashAllAboveFixupCommits(g *gocui.Gui, v *gocui.View) error {
|
||||
commit := gui.getSelectedCommit(g)
|
||||
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
commit := gui.getSelectedCommit()
|
||||
if commit == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -535,58 +550,11 @@ func (gui *Gui) handleSquashAllAboveFixupCommits(g *gocui.Gui, v *gocui.View) er
|
||||
}, nil)
|
||||
}
|
||||
|
||||
type resetOption struct {
|
||||
description string
|
||||
command string
|
||||
}
|
||||
|
||||
// GetDisplayStrings is a function.
|
||||
func (r *resetOption) GetDisplayStrings(isFocused bool) []string {
|
||||
return []string{r.description, color.New(color.FgRed).Sprint(r.command)}
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateCommitResetMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
commit := gui.getSelectedCommit(g)
|
||||
if commit == nil {
|
||||
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NoCommitsThisBranch"))
|
||||
}
|
||||
|
||||
strengths := []string{"soft", "mixed", "hard"}
|
||||
options := make([]*resetOption, len(strengths))
|
||||
for i, strength := range strengths {
|
||||
options[i] = &resetOption{
|
||||
description: fmt.Sprintf("%s reset", strength),
|
||||
command: fmt.Sprintf("reset --%s %s", strength, commit.Sha),
|
||||
}
|
||||
}
|
||||
|
||||
handleMenuPress := func(index int) error {
|
||||
if err := gui.GitCommand.ResetToCommit(commit.Sha, strengths[index]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.refreshCommits(g); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.refreshFiles(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.resetOrigin(gui.getCommitsView()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.State.Panels.Commits.SelectedLine = 0
|
||||
return gui.handleCommitSelect(g, gui.getCommitsView())
|
||||
}
|
||||
|
||||
return gui.createMenu(fmt.Sprintf("%s %s", gui.Tr.SLocalize("resetTo"), commit.Sha), options, len(options), handleMenuPress)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleTagCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
// TODO: bring up menu asking if you want to make a lightweight or annotated tag
|
||||
// if annotated, switch to a subprocess to create the message
|
||||
|
||||
commit := gui.getSelectedCommit(g)
|
||||
commit := gui.getSelectedCommit()
|
||||
if commit == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -597,26 +565,20 @@ func (gui *Gui) handleTagCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleCreateLightweightTag(commitSha string) error {
|
||||
return gui.createPromptPanel(gui.g, gui.getCommitsView(), gui.Tr.SLocalize("TagNameTitle"), "", func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.CreateLightweightTag(v.Buffer(), commitSha); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
if err := gui.refreshCommits(g); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
if err := gui.refreshTags(); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
return gui.handleCommitSelect(g, v)
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{COMMITS, TAGS}})
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCheckoutCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
commit := gui.getSelectedCommit(g)
|
||||
commit := gui.getSelectedCommit()
|
||||
if commit == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.createConfirmationPanel(g, gui.getCommitsView(), true, gui.Tr.SLocalize("checkoutCommit"), gui.Tr.SLocalize("SureCheckoutThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.handleCheckoutRef(commit.Sha)
|
||||
return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{})
|
||||
}, nil)
|
||||
}
|
||||
|
||||
@@ -624,9 +586,8 @@ func (gui *Gui) renderBranchCommitsWithSelection() error {
|
||||
commitsView := gui.getCommitsView()
|
||||
|
||||
gui.refreshSelectedLine(&gui.State.Panels.Commits.SelectedLine, len(gui.State.Commits))
|
||||
if err := gui.renderListPanel(commitsView, gui.State.Commits); err != nil {
|
||||
return err
|
||||
}
|
||||
displayStrings := presentation.GetCommitListDisplayStrings(gui.State.Commits, gui.State.ScreenMode != SCREEN_NORMAL, gui.cherryPickedCommitShaMap(), gui.State.Diff.Ref)
|
||||
gui.renderDisplayStrings(commitsView, displayStrings)
|
||||
if gui.g.CurrentView() == commitsView && commitsView.Context == "branch-commits" {
|
||||
if err := gui.handleCommitSelect(gui.g, commitsView); err != nil {
|
||||
return err
|
||||
@@ -647,6 +608,9 @@ func (gui *Gui) onCommitsTabClick(tabIndex int) error {
|
||||
func (gui *Gui) switchCommitsPanelContext(context string) error {
|
||||
commitsView := gui.getCommitsView()
|
||||
commitsView.Context = context
|
||||
if err := gui.onSearchEscape(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
contextTabIndexMap := map[string]int{
|
||||
"branch-commits": 0,
|
||||
@@ -655,7 +619,13 @@ func (gui *Gui) switchCommitsPanelContext(context string) error {
|
||||
|
||||
commitsView.TabIndex = contextTabIndexMap[context]
|
||||
|
||||
switch context {
|
||||
return gui.refreshCommitsViewWithSelection()
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshCommitsViewWithSelection() error {
|
||||
commitsView := gui.getCommitsView()
|
||||
|
||||
switch commitsView.Context {
|
||||
case "branch-commits":
|
||||
return gui.renderBranchCommitsWithSelection()
|
||||
case "reflog-commits":
|
||||
@@ -676,3 +646,69 @@ func (gui *Gui) handlePrevCommitsTab(g *gocui.Gui, v *gocui.View) error {
|
||||
utils.ModuloWithWrap(v.TabIndex-1, len(v.Tabs)),
|
||||
)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateCommitResetMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
commit := gui.getSelectedCommit()
|
||||
if commit == nil {
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("NoCommitsThisBranch"))
|
||||
}
|
||||
|
||||
return gui.createResetMenu(commit.Sha)
|
||||
}
|
||||
|
||||
func (gui *Gui) onCommitsPanelSearchSelect(selectedLine int) error {
|
||||
commitsView := gui.getCommitsView()
|
||||
switch commitsView.Context {
|
||||
case "branch-commits":
|
||||
gui.State.Panels.Commits.SelectedLine = selectedLine
|
||||
return gui.handleCommitSelect(gui.g, commitsView)
|
||||
case "reflog-commits":
|
||||
gui.State.Panels.ReflogCommits.SelectedLine = selectedLine
|
||||
return gui.handleReflogCommitSelect(gui.g, commitsView)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleOpenSearchForCommitsPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
// we usually lazyload these commits but now that we're searching we need to load them now
|
||||
if gui.State.Panels.Commits.LimitCommits {
|
||||
gui.State.Panels.Commits.LimitCommits = false
|
||||
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{COMMITS}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return gui.handleOpenSearch(gui.g, v)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleResetCherryPick(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.State.CherryPickedCommits = []*commands.Commit{}
|
||||
return gui.renderBranchCommitsWithSelection()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleGotoBottomForCommitsPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
// we usually lazyload these commits but now that we're searching we need to load them now
|
||||
if gui.State.Panels.Commits.LimitCommits {
|
||||
gui.State.Panels.Commits.LimitCommits = false
|
||||
if err := gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []int{COMMITS}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, view := range gui.getListViews() {
|
||||
if view.viewName == "commits" {
|
||||
return view.handleGotoBottom(g, v)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleClipboardCopyCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
commit := gui.getSelectedCommit()
|
||||
if commit == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.OSCommand.CopyToClipboard(commit.Sha)
|
||||
}
|
||||
|
||||
@@ -39,7 +39,8 @@ func (gui *Gui) closeConfirmationPrompt(g *gocui.Gui, returnFocusOnClose bool) e
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
g.DeleteKeybindings("confirmation")
|
||||
g.DeleteKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone)
|
||||
g.DeleteKeybinding("confirmation", gocui.KeyEsc, gocui.ModNone)
|
||||
return g.DeleteView("confirmation")
|
||||
}
|
||||
|
||||
@@ -61,6 +62,9 @@ func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, wrap bool, prompt s
|
||||
width, height := g.Size()
|
||||
panelWidth := 4 * width / 7
|
||||
panelHeight := gui.getMessageHeight(wrap, prompt, panelWidth)
|
||||
if panelHeight > height*3/4 {
|
||||
panelHeight = height * 3 / 4
|
||||
}
|
||||
return width/2 - panelWidth/2,
|
||||
height/2 - panelHeight/2 - panelHeight%2 - 1,
|
||||
width/2 + panelWidth/2,
|
||||
@@ -75,6 +79,9 @@ func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt
|
||||
return nil, err
|
||||
}
|
||||
confirmationView.HasLoader = hasLoader
|
||||
if hasLoader {
|
||||
gui.g.StartTicking()
|
||||
}
|
||||
confirmationView.Title = title
|
||||
confirmationView.Wrap = true
|
||||
confirmationView.FgColor = theme.GocuiDefaultTextColor
|
||||
@@ -119,9 +126,7 @@ func (gui *Gui) createPopupPanel(g *gocui.Gui, currentView *gocui.View, title, p
|
||||
}()
|
||||
}
|
||||
|
||||
if err := gui.renderString(g, "confirmation", prompt); err != nil {
|
||||
return err
|
||||
}
|
||||
gui.renderString(g, "confirmation", prompt)
|
||||
return gui.setKeyBindings(g, handleConfirm, handleClose, returnFocusOnClose)
|
||||
})
|
||||
return nil
|
||||
@@ -148,19 +153,14 @@ func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*go
|
||||
"keyBindConfirm": "enter",
|
||||
},
|
||||
)
|
||||
if err := gui.renderString(g, "options", actions); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.renderString(g, "options", actions)
|
||||
if err := g.SetKeybinding("confirmation", nil, gocui.KeyEnter, gocui.ModNone, gui.wrappedConfirmationFunction(handleConfirm, returnFocusOnClose)); err != nil {
|
||||
return err
|
||||
}
|
||||
return g.SetKeybinding("confirmation", nil, gocui.KeyEsc, gocui.ModNone, gui.wrappedConfirmationFunction(handleClose, returnFocusOnClose))
|
||||
}
|
||||
|
||||
func (gui *Gui) createMessagePanel(g *gocui.Gui, currentView *gocui.View, title, prompt string) error {
|
||||
return gui.createPopupPanel(g, currentView, title, prompt, false, true, false, nil, nil)
|
||||
}
|
||||
|
||||
// createSpecificErrorPanel allows you to create an error popup, specifying the
|
||||
// view to be focused when the user closes the popup, and a boolean specifying
|
||||
// whether we will log the error. If the message may include a user password,
|
||||
@@ -179,9 +179,17 @@ func (gui *Gui) createSpecificErrorPanel(message string, nextView *gocui.View, w
|
||||
|
||||
colorFunction := color.New(color.FgRed).SprintFunc()
|
||||
coloredMessage := colorFunction(strings.TrimSpace(message))
|
||||
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.createConfirmationPanel(gui.g, nextView, true, gui.Tr.SLocalize("Error"), coloredMessage, nil, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) createErrorPanel(g *gocui.Gui, message string) error {
|
||||
return gui.createSpecificErrorPanel(message, g.CurrentView(), true)
|
||||
func (gui *Gui) createErrorPanel(message string) error {
|
||||
return gui.createSpecificErrorPanel(message, gui.g.CurrentView(), true)
|
||||
}
|
||||
|
||||
func (gui *Gui) surfaceError(err error) error {
|
||||
return gui.createErrorPanel(err.Error())
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ package gui
|
||||
// which currently just means a context that affects both the main and secondary views
|
||||
// other views can have their context changed directly but this function helps
|
||||
// keep the main and secondary views in sync
|
||||
func (gui *Gui) changeMainViewsContext(context string) error {
|
||||
func (gui *Gui) changeMainViewsContext(context string) {
|
||||
if gui.State.MainContext == context {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
switch context {
|
||||
@@ -16,5 +16,4 @@ func (gui *Gui) changeMainViewsContext(context string) error {
|
||||
}
|
||||
|
||||
gui.State.MainContext = context
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
|
||||
type credentials chan string
|
||||
|
||||
// waitForPassUname wait for a username or password input from the credentials popup
|
||||
func (gui *Gui) waitForPassUname(g *gocui.Gui, currentView *gocui.View, passOrUname string) string {
|
||||
// promptUserForCredential wait for a username or password input from the credentials popup
|
||||
func (gui *Gui) promptUserForCredential(passOrUname string) string {
|
||||
gui.credentials = make(chan string)
|
||||
g.Update(func(g *gocui.Gui) error {
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
credentialsView, _ := g.View("credentials")
|
||||
if passOrUname == "username" {
|
||||
credentialsView.Title = gui.Tr.SLocalize("CredentialsUsername")
|
||||
@@ -20,7 +20,7 @@ func (gui *Gui) waitForPassUname(g *gocui.Gui, currentView *gocui.View, passOrUn
|
||||
credentialsView.Title = gui.Tr.SLocalize("CredentialsPassword")
|
||||
credentialsView.Mask = '*'
|
||||
}
|
||||
err := gui.switchFocus(g, currentView, credentialsView)
|
||||
err := gui.switchFocus(g, gui.g.CurrentView(), credentialsView)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -36,19 +36,9 @@ func (gui *Gui) waitForPassUname(g *gocui.Gui, currentView *gocui.View, passOrUn
|
||||
func (gui *Gui) handleSubmitCredential(g *gocui.Gui, v *gocui.View) error {
|
||||
message := gui.trimmedContent(v)
|
||||
gui.credentials <- message
|
||||
err := gui.refreshFiles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.Clear()
|
||||
err = v.SetCursor(0, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = g.SetViewOnBottom("credentials")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = v.SetCursor(0, 0)
|
||||
_, _ = g.SetViewOnBottom("credentials")
|
||||
nextView, err := gui.g.View("confirmation")
|
||||
if err != nil {
|
||||
nextView = gui.getFilesView()
|
||||
@@ -57,7 +47,7 @@ func (gui *Gui) handleSubmitCredential(g *gocui.Gui, v *gocui.View) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.refreshCommits(g)
|
||||
return gui.refreshSidePanels(refreshOptions{})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCloseCredentialsView(g *gocui.Gui, v *gocui.View) error {
|
||||
@@ -82,14 +72,13 @@ func (gui *Gui) handleCredentialsViewFocused(g *gocui.Gui, v *gocui.View) error
|
||||
"keyBindConfirm": "enter",
|
||||
},
|
||||
)
|
||||
return gui.renderString(g, "options", message)
|
||||
gui.renderString(g, "options", message)
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleCredentialsPopup handles the views after executing a command that might ask for credentials
|
||||
func (gui *Gui) HandleCredentialsPopup(g *gocui.Gui, popupOpened bool, cmdErr error) {
|
||||
if popupOpened {
|
||||
_, _ = gui.g.SetViewOnBottom("credentials")
|
||||
}
|
||||
// handleCredentialsPopup handles the views after executing a command that might ask for credentials
|
||||
func (gui *Gui) handleCredentialsPopup(cmdErr error) {
|
||||
_, _ = gui.g.SetViewOnBottom("credentials")
|
||||
if cmdErr != nil {
|
||||
errMessage := cmdErr.Error()
|
||||
if strings.Contains(errMessage, "Invalid username or password") {
|
||||
@@ -98,7 +87,6 @@ func (gui *Gui) HandleCredentialsPopup(g *gocui.Gui, popupOpened bool, cmdErr er
|
||||
// we are not logging this error because it may contain a password
|
||||
_ = gui.createSpecificErrorPanel(errMessage, gui.getFilesView(), false)
|
||||
} else {
|
||||
_ = gui.closeConfirmationPrompt(g, true)
|
||||
_ = gui.refreshSidePanels(g)
|
||||
_ = gui.closeConfirmationPrompt(gui.g, true)
|
||||
}
|
||||
}
|
||||
|
||||
181
pkg/gui/diffing.go
Normal file
181
pkg/gui/diffing.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
)
|
||||
|
||||
func (gui *Gui) inDiffMode() bool {
|
||||
return gui.State.Diff.Ref != ""
|
||||
}
|
||||
|
||||
func (gui *Gui) exitDiffMode() error {
|
||||
gui.State.Diff = DiffState{}
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}
|
||||
|
||||
func (gui *Gui) renderDiff() error {
|
||||
gui.getMainView().Title = "Diff"
|
||||
gui.State.SplitMainPanel = false
|
||||
filterArg := ""
|
||||
if gui.inFilterMode() {
|
||||
filterArg = fmt.Sprintf(" -- %s", gui.State.FilterPath)
|
||||
}
|
||||
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
fmt.Sprintf("git diff --color %s %s", gui.diffStr(), filterArg),
|
||||
)
|
||||
if err := gui.newPtyTask("main", cmd); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// currentDiffTerminals returns the current diff terminals of the currently selected item.
|
||||
// in the case of a branch it returns both the branch and it's upstream name,
|
||||
// which becomes an option when you bring up the diff menu, but when you're just
|
||||
// flicking through branches it will be using the local branch name.
|
||||
func (gui *Gui) currentDiffTerminals() []string {
|
||||
currentView := gui.g.CurrentView()
|
||||
if currentView == nil {
|
||||
return nil
|
||||
}
|
||||
names := []string{}
|
||||
switch currentView.Name() {
|
||||
case "files":
|
||||
// not supporting files for now
|
||||
// file, err := gui.getSelectedFile()
|
||||
// if err == nil {
|
||||
// names = append(names, file.Name)
|
||||
// }
|
||||
case "commitFiles":
|
||||
// not supporting commit files for now
|
||||
// file := gui.getSelectedCommitFile()
|
||||
// if file != nil {
|
||||
// names = append(names, file.Name)
|
||||
// }
|
||||
case "commits":
|
||||
var commit *commands.Commit
|
||||
switch gui.getCommitsView().Context {
|
||||
case "reflog-commits":
|
||||
commit = gui.getSelectedReflogCommit()
|
||||
case "branch-commits":
|
||||
commit = gui.getSelectedCommit()
|
||||
}
|
||||
if commit != nil {
|
||||
names = append(names, commit.Sha)
|
||||
}
|
||||
case "stash":
|
||||
entry := gui.getSelectedStashEntry()
|
||||
if entry != nil {
|
||||
names = append(names, entry.RefName())
|
||||
}
|
||||
case "branches":
|
||||
switch gui.getBranchesView().Context {
|
||||
case "local-branches":
|
||||
branch := gui.getSelectedBranch()
|
||||
if branch != nil {
|
||||
names = append(names, branch.Name)
|
||||
if branch.UpstreamName != "" {
|
||||
names = append(names, branch.UpstreamName)
|
||||
}
|
||||
}
|
||||
case "remotes":
|
||||
remote := gui.getSelectedRemote()
|
||||
if remote != nil {
|
||||
names = append(names, remote.Name)
|
||||
}
|
||||
case "remote-branches":
|
||||
remoteBranch := gui.getSelectedRemoteBranch()
|
||||
if remoteBranch != nil {
|
||||
names = append(names, remoteBranch.FullName())
|
||||
}
|
||||
case "tags":
|
||||
tag := gui.getSelectedTag()
|
||||
if tag != nil {
|
||||
names = append(names, tag.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func (gui *Gui) currentDiffTerminal() string {
|
||||
names := gui.currentDiffTerminals()
|
||||
if len(names) == 0 {
|
||||
return ""
|
||||
}
|
||||
return names[0]
|
||||
}
|
||||
|
||||
func (gui *Gui) diffStr() string {
|
||||
output := gui.State.Diff.Ref
|
||||
|
||||
right := gui.currentDiffTerminal()
|
||||
if right != "" {
|
||||
output += " " + right
|
||||
}
|
||||
if gui.State.Diff.Reverse {
|
||||
output += " -R"
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateDiffingMenuPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
names := gui.currentDiffTerminals()
|
||||
|
||||
menuItems := []*menuItem{}
|
||||
for _, name := range names {
|
||||
name := name
|
||||
menuItems = append(menuItems, []*menuItem{
|
||||
{
|
||||
displayString: fmt.Sprintf("%s %s", gui.Tr.SLocalize("diff"), name),
|
||||
onPress: func() error {
|
||||
gui.State.Diff.Ref = name
|
||||
// can scope this down based on current view but too lazy right now
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
},
|
||||
},
|
||||
}...)
|
||||
}
|
||||
|
||||
menuItems = append(menuItems, []*menuItem{
|
||||
{
|
||||
displayString: gui.Tr.SLocalize("enterRefToDiff"),
|
||||
onPress: func() error {
|
||||
return gui.createPromptPanel(gui.g, v, gui.Tr.SLocalize("enteRefName"), "", func(g *gocui.Gui, promptView *gocui.View) error {
|
||||
gui.State.Diff.Ref = strings.TrimSpace(promptView.Buffer())
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
})
|
||||
},
|
||||
},
|
||||
}...)
|
||||
|
||||
if gui.inDiffMode() {
|
||||
menuItems = append(menuItems, []*menuItem{
|
||||
{
|
||||
displayString: gui.Tr.SLocalize("swapDiff"),
|
||||
onPress: func() error {
|
||||
gui.State.Diff.Reverse = !gui.State.Diff.Reverse
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: gui.Tr.SLocalize("exitDiffMode"),
|
||||
onPress: func() error {
|
||||
gui.State.Diff = DiffState{}
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
},
|
||||
},
|
||||
}...)
|
||||
}
|
||||
|
||||
return gui.createMenu(gui.Tr.SLocalize("DiffingMenuTitle"), menuItems, createMenuOptions{showCancel: true})
|
||||
}
|
||||
42
pkg/gui/discard_changes_menu_panel.go
Normal file
42
pkg/gui/discard_changes_menu_panel.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
func (gui *Gui) handleCreateDiscardMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
file, err := gui.getSelectedFile()
|
||||
if err != nil {
|
||||
if err != gui.Errors.ErrNoFiles {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
menuItems := []*menuItem{
|
||||
{
|
||||
displayString: gui.Tr.SLocalize("discardAllChanges"),
|
||||
onPress: func() error {
|
||||
if err := gui.GitCommand.DiscardAllFileChanges(file); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if file.HasStagedChanges && file.HasUnstagedChanges {
|
||||
menuItems = append(menuItems, &menuItem{
|
||||
displayString: gui.Tr.SLocalize("discardUnstagedChanges"),
|
||||
onPress: func() error {
|
||||
if err := gui.GitCommand.DiscardUnstagedFileChanges(file); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return gui.createMenu(file.Name, menuItems, createMenuOptions{showCancel: true})
|
||||
}
|
||||
@@ -19,13 +19,22 @@ type fileWatcher struct {
|
||||
Watcher *fsnotify.Watcher
|
||||
WatchedFilenames []string
|
||||
Log *logrus.Entry
|
||||
Disabled bool
|
||||
}
|
||||
|
||||
func NewFileWatcher(log *logrus.Entry) *fileWatcher {
|
||||
// TODO: get this going again, and ensure we don't see any crashes from it
|
||||
return &fileWatcher{
|
||||
Disabled: true,
|
||||
}
|
||||
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return nil
|
||||
return &fileWatcher{
|
||||
Disabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
return &fileWatcher{
|
||||
@@ -66,6 +75,10 @@ func (w *fileWatcher) watchFilename(filename string) {
|
||||
}
|
||||
|
||||
func (w *fileWatcher) addFilesToFileWatcher(files []*commands.File) error {
|
||||
if w.Disabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -105,7 +118,7 @@ func min(a int, b int) int {
|
||||
// TODO: consider watching the whole directory recursively (could be more expensive)
|
||||
func (gui *Gui) watchFilesForChanges() {
|
||||
gui.fileWatcher = NewFileWatcher(gui.Log)
|
||||
if gui.fileWatcher == nil {
|
||||
if gui.fileWatcher.Disabled {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
@@ -119,12 +132,7 @@ func (gui *Gui) watchFilesForChanges() {
|
||||
}
|
||||
// only refresh if we're not already
|
||||
if !gui.State.IsRefreshingFiles {
|
||||
if err := gui.refreshFiles(); err != nil {
|
||||
err = gui.createErrorPanel(gui.g, err.Error())
|
||||
if err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
}
|
||||
gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
|
||||
}
|
||||
|
||||
// watch for errors
|
||||
|
||||
@@ -8,16 +8,18 @@ import (
|
||||
// "strings"
|
||||
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// list panel functions
|
||||
|
||||
func (gui *Gui) getSelectedFile(g *gocui.Gui) (*commands.File, error) {
|
||||
func (gui *Gui) getSelectedFile() (*commands.File, error) {
|
||||
selectedLine := gui.State.Panels.Files.SelectedLine
|
||||
if selectedLine == -1 {
|
||||
return &commands.File{}, gui.Errors.ErrNoFiles
|
||||
@@ -27,16 +29,29 @@ func (gui *Gui) getSelectedFile(g *gocui.Gui) (*commands.File, error) {
|
||||
}
|
||||
|
||||
func (gui *Gui) selectFile(alreadySelected bool) error {
|
||||
file, err := gui.getSelectedFile(gui.g)
|
||||
gui.getFilesView().FocusPoint(0, gui.State.Panels.Files.SelectedLine)
|
||||
|
||||
if gui.inDiffMode() {
|
||||
return gui.renderDiff()
|
||||
}
|
||||
|
||||
file, err := gui.getSelectedFile()
|
||||
if err != nil {
|
||||
if err != gui.Errors.ErrNoFiles {
|
||||
return err
|
||||
}
|
||||
return gui.renderString(gui.g, "main", gui.Tr.SLocalize("NoChangedFiles"))
|
||||
gui.State.SplitMainPanel = false
|
||||
gui.getMainView().Title = ""
|
||||
return gui.newStringTask("main", gui.Tr.SLocalize("NoChangedFiles"))
|
||||
}
|
||||
|
||||
if err := gui.focusPoint(0, gui.State.Panels.Files.SelectedLine, len(gui.State.Files), gui.getFilesView()); err != nil {
|
||||
return err
|
||||
if !alreadySelected {
|
||||
if err := gui.resetOrigin(gui.getMainView()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.resetOrigin(gui.getSecondaryView()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if file.HasInlineMergeConflicts {
|
||||
@@ -45,37 +60,31 @@ func (gui *Gui) selectFile(alreadySelected bool) error {
|
||||
return gui.refreshMergePanel()
|
||||
}
|
||||
|
||||
content := gui.GitCommand.Diff(file, false, false)
|
||||
contentCached := gui.GitCommand.Diff(file, false, true)
|
||||
leftContent := content
|
||||
if file.HasStagedChanges && file.HasUnstagedChanges {
|
||||
gui.State.SplitMainPanel = true
|
||||
gui.getMainView().Title = gui.Tr.SLocalize("UnstagedChanges")
|
||||
gui.getSecondaryView().Title = gui.Tr.SLocalize("StagedChanges")
|
||||
cmdStr := gui.GitCommand.DiffCmdStr(file, false, true)
|
||||
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
|
||||
if err := gui.newPtyTask("secondary", cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
gui.State.SplitMainPanel = false
|
||||
if file.HasUnstagedChanges {
|
||||
leftContent = content
|
||||
gui.getMainView().Title = gui.Tr.SLocalize("UnstagedChanges")
|
||||
} else {
|
||||
leftContent = contentCached
|
||||
gui.getMainView().Title = gui.Tr.SLocalize("StagedChanges")
|
||||
}
|
||||
}
|
||||
|
||||
if alreadySelected {
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
if err := gui.setViewContent(gui.g, gui.getSecondaryView(), contentCached); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.setViewContent(gui.g, gui.getMainView(), leftContent)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
if err := gui.renderString(gui.g, "secondary", contentCached); err != nil {
|
||||
cmdStr := gui.GitCommand.DiffCmdStr(file, false, !file.HasUnstagedChanges && file.HasStagedChanges)
|
||||
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
|
||||
if err := gui.newPtyTask("main", cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.renderString(gui.g, "main", leftContent)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshFiles() error {
|
||||
@@ -86,7 +95,7 @@ func (gui *Gui) refreshFiles() error {
|
||||
gui.State.RefreshingFilesMutex.Unlock()
|
||||
}()
|
||||
|
||||
selectedFile, _ := gui.getSelectedFile(gui.g)
|
||||
selectedFile, _ := gui.getSelectedFile()
|
||||
|
||||
filesView := gui.getFilesView()
|
||||
if filesView == nil {
|
||||
@@ -98,17 +107,11 @@ func (gui *Gui) refreshFiles() error {
|
||||
}
|
||||
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
|
||||
filesView.Clear()
|
||||
isFocused := gui.g.CurrentView().Name() == "files"
|
||||
list, err := utils.RenderList(gui.State.Files, isFocused)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprint(filesView, list)
|
||||
displayStrings := presentation.GetFileListDisplayStrings(gui.State.Files, gui.State.Diff.Ref)
|
||||
gui.renderDisplayStrings(filesView, displayStrings)
|
||||
|
||||
if g.CurrentView() == filesView || (g.CurrentView() == gui.getMainView() && g.CurrentView().Context == "merging") {
|
||||
newSelectedFile, _ := gui.getSelectedFile(gui.g)
|
||||
newSelectedFile, _ := gui.getSelectedFile()
|
||||
alreadySelected := newSelectedFile.Name == selectedFile.Name
|
||||
return gui.selectFile(alreadySelected)
|
||||
}
|
||||
@@ -133,7 +136,7 @@ func (gui *Gui) stagedFiles() []*commands.File {
|
||||
|
||||
func (gui *Gui) trackedFiles() []*commands.File {
|
||||
files := gui.State.Files
|
||||
result := make([]*commands.File, 0)
|
||||
result := make([]*commands.File, 0, len(files))
|
||||
for _, file := range files {
|
||||
if file.Tracked {
|
||||
result = append(result, file)
|
||||
@@ -143,7 +146,7 @@ func (gui *Gui) trackedFiles() []*commands.File {
|
||||
}
|
||||
|
||||
func (gui *Gui) stageSelectedFile(g *gocui.Gui) error {
|
||||
file, err := gui.getSelectedFile(g)
|
||||
file, err := gui.getSelectedFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -155,7 +158,7 @@ func (gui *Gui) handleEnterFile(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) enterFile(forceSecondaryFocused bool, selectedLineIdx int) error {
|
||||
file, err := gui.getSelectedFile(gui.g)
|
||||
file, err := gui.getSelectedFile()
|
||||
if err != nil {
|
||||
if err != gui.Errors.ErrNoFiles {
|
||||
return err
|
||||
@@ -166,11 +169,9 @@ func (gui *Gui) enterFile(forceSecondaryFocused bool, selectedLineIdx int) error
|
||||
return gui.handleSwitchToMerge(gui.g, gui.getFilesView())
|
||||
}
|
||||
if file.HasMergeConflicts {
|
||||
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("FileStagingRequirements"))
|
||||
}
|
||||
if err := gui.changeMainViewsContext("staging"); err != nil {
|
||||
return err
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("FileStagingRequirements"))
|
||||
}
|
||||
gui.changeMainViewsContext("staging")
|
||||
if err := gui.switchFocus(gui.g, gui.getFilesView(), gui.getMainView()); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -178,7 +179,7 @@ func (gui *Gui) enterFile(forceSecondaryFocused bool, selectedLineIdx int) error
|
||||
}
|
||||
|
||||
func (gui *Gui) handleFilePress(g *gocui.Gui, v *gocui.View) error {
|
||||
file, err := gui.getSelectedFile(g)
|
||||
file, err := gui.getSelectedFile()
|
||||
if err != nil {
|
||||
if err == gui.Errors.ErrNoFiles {
|
||||
return nil
|
||||
@@ -191,12 +192,15 @@ func (gui *Gui) handleFilePress(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
if file.HasUnstagedChanges {
|
||||
gui.GitCommand.StageFile(file.Name)
|
||||
err = gui.GitCommand.StageFile(file.Name)
|
||||
} else {
|
||||
gui.GitCommand.UnStageFile(file.Name, file.Tracked)
|
||||
err = gui.GitCommand.UnStageFile(file.Name, file.Tracked)
|
||||
}
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
if err := gui.refreshFiles(); err != nil {
|
||||
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -228,10 +232,10 @@ func (gui *Gui) handleStageAll(g *gocui.Gui, v *gocui.View) error {
|
||||
err = gui.GitCommand.StageAll()
|
||||
}
|
||||
if err != nil {
|
||||
_ = gui.createErrorPanel(g, err.Error())
|
||||
_ = gui.surfaceError(err)
|
||||
}
|
||||
|
||||
if err := gui.refreshFiles(); err != nil {
|
||||
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -239,28 +243,39 @@ func (gui *Gui) handleStageAll(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
|
||||
file, err := gui.getSelectedFile(g)
|
||||
file, err := gui.getSelectedFile()
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
if file.Tracked {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantIgnoreTrackFiles"))
|
||||
return gui.createConfirmationPanel(gui.g, gui.g.CurrentView(), true, gui.Tr.SLocalize("IgnoreTracked"), gui.Tr.SLocalize("IgnoreTrackedPrompt"),
|
||||
// On confirmation
|
||||
func(_ *gocui.Gui, _ *gocui.View) error {
|
||||
if err := gui.GitCommand.Ignore(file.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.GitCommand.RemoveTrackedFiles(file.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{FILES}})
|
||||
}, nil)
|
||||
}
|
||||
|
||||
if err := gui.GitCommand.Ignore(file.Name); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshFiles()
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{FILES}})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleWIPCommitPress(g *gocui.Gui, filesView *gocui.View) error {
|
||||
skipHookPreifx := gui.Config.GetUserConfig().GetString("git.skipHookPrefix")
|
||||
if skipHookPreifx == "" {
|
||||
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("SkipHookPrefixNotConfigured"))
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("SkipHookPrefixNotConfigured"))
|
||||
}
|
||||
|
||||
if err := gui.renderString(g, "commitMessage", skipHookPreifx); err != nil {
|
||||
return err
|
||||
}
|
||||
gui.renderString(g, "commitMessage", skipHookPreifx)
|
||||
if err := gui.getCommitMessageView().SetCursor(len(skipHookPreifx), 0); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -269,25 +284,66 @@ func (gui *Gui) handleWIPCommitPress(g *gocui.Gui, filesView *gocui.View) error
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
|
||||
if len(gui.stagedFiles()) == 0 && gui.State.WorkingTreeState == "normal" {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit"))
|
||||
if len(gui.stagedFiles()) == 0 {
|
||||
return gui.promptToStageAllAndRetry(func() error {
|
||||
return gui.handleCommitPress(gui.g, filesView)
|
||||
})
|
||||
}
|
||||
|
||||
commitMessageView := gui.getCommitMessageView()
|
||||
prefixPattern := gui.Config.GetUserConfig().GetString("git.commitPrefixes." + utils.GetCurrentRepoName() + ".pattern")
|
||||
prefixReplace := gui.Config.GetUserConfig().GetString("git.commitPrefixes." + utils.GetCurrentRepoName() + ".replace")
|
||||
if len(prefixPattern) > 0 && len(prefixReplace) > 0 {
|
||||
rgx, err := regexp.Compile(prefixPattern)
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(fmt.Sprintf("%s: %s", gui.Tr.SLocalize("commitPrefixPatternError"), err.Error()))
|
||||
}
|
||||
prefix := rgx.ReplaceAllString(gui.getCheckedOutBranch().Name, prefixReplace)
|
||||
gui.renderString(g, "commitMessage", prefix)
|
||||
if err := commitMessageView.SetCursor(len(prefix), 0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
g.Update(func(g *gocui.Gui) error {
|
||||
g.SetViewOnTop("commitMessage")
|
||||
gui.switchFocus(g, filesView, commitMessageView)
|
||||
if _, err := g.SetViewOnTop("commitMessage"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.switchFocus(g, filesView, commitMessageView); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.RenderCommitLength()
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) promptToStageAllAndRetry(retry func() error) error {
|
||||
return gui.createConfirmationPanel(
|
||||
gui.g, gui.getFilesView(), true, gui.Tr.SLocalize("NoFilesStagedTitle"), gui.Tr.SLocalize("NoFilesStagedPrompt"),
|
||||
func(*gocui.Gui, *gocui.View) error {
|
||||
if err := gui.GitCommand.StageAll(); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
if err := gui.refreshFiles(); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return retry()
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleAmendCommitPress(g *gocui.Gui, filesView *gocui.View) error {
|
||||
if len(gui.stagedFiles()) == 0 && gui.State.WorkingTreeState == "normal" {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit"))
|
||||
if len(gui.stagedFiles()) == 0 {
|
||||
return gui.promptToStageAllAndRetry(func() error {
|
||||
return gui.handleAmendCommitPress(gui.g, filesView)
|
||||
})
|
||||
}
|
||||
|
||||
if len(gui.State.Commits) == 0 {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoCommitToAmend"))
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("NoCommitToAmend"))
|
||||
}
|
||||
|
||||
title := strings.Title(gui.Tr.SLocalize("AmendLastCommit"))
|
||||
@@ -302,16 +358,19 @@ func (gui *Gui) handleAmendCommitPress(g *gocui.Gui, filesView *gocui.View) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(g)
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}, nil)
|
||||
}
|
||||
|
||||
// handleCommitEditorPress - handle when the user wants to commit changes via
|
||||
// their editor rather than via the popup panel
|
||||
func (gui *Gui) handleCommitEditorPress(g *gocui.Gui, filesView *gocui.View) error {
|
||||
if len(gui.stagedFiles()) == 0 && gui.State.WorkingTreeState == "normal" {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit"))
|
||||
if len(gui.stagedFiles()) == 0 {
|
||||
return gui.promptToStageAllAndRetry(func() error {
|
||||
return gui.handleCommitEditorPress(gui.g, filesView)
|
||||
})
|
||||
}
|
||||
|
||||
gui.PrepareSubProcess(g, "git", "commit")
|
||||
return nil
|
||||
}
|
||||
@@ -330,167 +389,193 @@ func (gui *Gui) editFile(filename string) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleFileEdit(g *gocui.Gui, v *gocui.View) error {
|
||||
file, err := gui.getSelectedFile(g)
|
||||
file, err := gui.getSelectedFile()
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.editFile(file.Name)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleFileOpen(g *gocui.Gui, v *gocui.View) error {
|
||||
file, err := gui.getSelectedFile(g)
|
||||
file, err := gui.getSelectedFile()
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.openFile(file.Name)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRefreshFiles(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.refreshFiles()
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{FILES}})
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshStateFiles() error {
|
||||
// keep track of where the cursor is currently and the current file names
|
||||
// when we refresh, go looking for a matching name
|
||||
// move the cursor to there.
|
||||
selectedFile, _ := gui.getSelectedFile()
|
||||
|
||||
// get files to stage
|
||||
files := gui.GitCommand.GetStatusFiles()
|
||||
gui.State.Files = gui.GitCommand.MergeStatusFiles(gui.State.Files, files)
|
||||
files := gui.GitCommand.GetStatusFiles(commands.GetStatusFileOptions{})
|
||||
gui.State.Files = gui.GitCommand.MergeStatusFiles(gui.State.Files, files, selectedFile)
|
||||
|
||||
if err := gui.fileWatcher.addFilesToFileWatcher(files); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.refreshSelectedLine(&gui.State.Panels.Files.SelectedLine, len(gui.State.Files))
|
||||
return gui.updateWorkTreeState()
|
||||
}
|
||||
|
||||
func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) {
|
||||
item, err := gui.getSelectedFile(g)
|
||||
if err != nil {
|
||||
if err != gui.Errors.ErrNoFiles {
|
||||
return "", err
|
||||
// let's try to find our file again and move the cursor to that
|
||||
for idx, f := range gui.State.Files {
|
||||
if selectedFile != nil && f.Matches(selectedFile) {
|
||||
gui.State.Panels.Files.SelectedLine = idx
|
||||
break
|
||||
}
|
||||
return "", gui.renderString(g, "main", gui.Tr.SLocalize("NoFilesDisplay"))
|
||||
}
|
||||
if item.Type != "file" {
|
||||
return "", gui.renderString(g, "main", gui.Tr.SLocalize("NotAFile"))
|
||||
}
|
||||
cat, err := gui.GitCommand.CatFile(item.Name)
|
||||
if err != nil {
|
||||
gui.Log.Error(err)
|
||||
return "", gui.renderString(g, "main", err.Error())
|
||||
}
|
||||
return cat, nil
|
||||
|
||||
gui.refreshSelectedLine(&gui.State.Panels.Files.SelectedLine, len(gui.State.Files))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handlePullFiles(g *gocui.Gui, v *gocui.View) error {
|
||||
// if we have no upstream branch we need to set that first
|
||||
_, pullables := gui.GitCommand.GetCurrentBranchUpstreamDifferenceCount()
|
||||
currentBranchName, err := gui.GitCommand.CurrentBranchName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if pullables == "?" {
|
||||
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("EnterUpstream"), "origin/"+currentBranchName, func(g *gocui.Gui, v *gocui.View) error {
|
||||
currentBranch := gui.currentBranch()
|
||||
if currentBranch.Pullables == "?" {
|
||||
// see if we have this branch in our config with an upstream
|
||||
conf, err := gui.GitCommand.Repo.Config()
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
for branchName, branch := range conf.Branches {
|
||||
if branchName == currentBranch.Name {
|
||||
return gui.pullFiles(PullFilesOptions{RemoteName: branch.Remote, BranchName: branch.Name})
|
||||
}
|
||||
}
|
||||
|
||||
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("EnterUpstream"), "origin/"+currentBranch.Name, func(g *gocui.Gui, v *gocui.View) error {
|
||||
upstream := gui.trimmedContent(v)
|
||||
if err := gui.GitCommand.SetUpstreamBranch(upstream); err != nil {
|
||||
errorMessage := err.Error()
|
||||
if strings.Contains(errorMessage, "does not exist") {
|
||||
errorMessage = fmt.Sprintf("upstream branch %s not found.\nIf you expect it to exist, you should fetch (with 'f').\nOtherwise, you should push (with 'shift+P')", upstream)
|
||||
}
|
||||
return gui.createErrorPanel(gui.g, errorMessage)
|
||||
return gui.createErrorPanel(errorMessage)
|
||||
}
|
||||
return gui.pullFiles(v)
|
||||
return gui.pullFiles(PullFilesOptions{})
|
||||
})
|
||||
}
|
||||
|
||||
return gui.pullFiles(v)
|
||||
return gui.pullFiles(PullFilesOptions{})
|
||||
}
|
||||
|
||||
func (gui *Gui) pullFiles(v *gocui.View) error {
|
||||
if err := gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("PullWait")); err != nil {
|
||||
type PullFilesOptions struct {
|
||||
RemoteName string
|
||||
BranchName string
|
||||
}
|
||||
|
||||
func (gui *Gui) pullFiles(opts PullFilesOptions) error {
|
||||
if err := gui.createLoaderPanel(gui.g, gui.g.CurrentView(), gui.Tr.SLocalize("PullWait")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
unamePassOpend := false
|
||||
err := gui.GitCommand.Pull(func(passOrUname string) string {
|
||||
unamePassOpend = true
|
||||
return gui.waitForPassUname(gui.g, v, passOrUname)
|
||||
})
|
||||
gui.HandleCredentialsPopup(gui.g, unamePassOpend, err)
|
||||
}()
|
||||
mode := gui.Config.GetUserConfig().GetString("git.pull.mode")
|
||||
|
||||
go gui.pullWithMode(mode, opts)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) pushWithForceFlag(g *gocui.Gui, v *gocui.View, force bool, upstream string) error {
|
||||
func (gui *Gui) pullWithMode(mode string, opts PullFilesOptions) error {
|
||||
err := gui.GitCommand.Fetch(
|
||||
commands.FetchOptions{
|
||||
PromptUserForCredential: gui.promptUserForCredential,
|
||||
RemoteName: opts.RemoteName,
|
||||
BranchName: opts.BranchName,
|
||||
},
|
||||
)
|
||||
gui.handleCredentialsPopup(err)
|
||||
if err != nil {
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case "rebase":
|
||||
err := gui.GitCommand.RebaseBranch("FETCH_HEAD")
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
case "merge":
|
||||
err := gui.GitCommand.Merge("FETCH_HEAD", commands.MergeOpts{})
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
case "ff-only":
|
||||
err := gui.GitCommand.Merge("FETCH_HEAD", commands.MergeOpts{FastForwardOnly: true})
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
default:
|
||||
return gui.createErrorPanel(fmt.Sprintf("git pull mode '%s' unrecognised", mode))
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) pushWithForceFlag(g *gocui.Gui, v *gocui.View, force bool, upstream string, args string) error {
|
||||
if err := gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("PushWait")); err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
unamePassOpend := false
|
||||
branchName := gui.getCheckedOutBranch().Name
|
||||
err := gui.GitCommand.Push(branchName, force, upstream, func(passOrUname string) string {
|
||||
unamePassOpend = true
|
||||
return gui.waitForPassUname(g, v, passOrUname)
|
||||
})
|
||||
gui.HandleCredentialsPopup(g, unamePassOpend, err)
|
||||
err := gui.GitCommand.Push(branchName, force, upstream, args, gui.promptUserForCredential)
|
||||
gui.handleCredentialsPopup(err)
|
||||
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error {
|
||||
// if we have pullables we'll ask if the user wants to force push
|
||||
_, pullables := gui.GitCommand.GetCurrentBranchUpstreamDifferenceCount()
|
||||
currentBranchName, err := gui.GitCommand.CurrentBranchName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currentBranch := gui.currentBranch()
|
||||
|
||||
if pullables == "?" {
|
||||
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("EnterUpstream"), "origin "+currentBranchName, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.pushWithForceFlag(g, v, false, gui.trimmedContent(v))
|
||||
})
|
||||
} else if pullables == "0" {
|
||||
return gui.pushWithForceFlag(g, v, false, "")
|
||||
if currentBranch.Pullables == "?" {
|
||||
// see if we have this branch in our config with an upstream
|
||||
conf, err := gui.GitCommand.Repo.Config()
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
for branchName, branch := range conf.Branches {
|
||||
if branchName == currentBranch.Name {
|
||||
return gui.pushWithForceFlag(g, v, false, "", fmt.Sprintf("%s %s", branch.Remote, branchName))
|
||||
}
|
||||
}
|
||||
|
||||
if gui.GitCommand.PushToCurrent {
|
||||
return gui.pushWithForceFlag(g, v, false, "", "--set-upstream")
|
||||
} else {
|
||||
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("EnterUpstream"), "origin "+currentBranch.Name, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.pushWithForceFlag(g, v, false, gui.trimmedContent(v), "")
|
||||
})
|
||||
}
|
||||
} else if currentBranch.Pullables == "0" {
|
||||
return gui.pushWithForceFlag(g, v, false, "", "")
|
||||
}
|
||||
return gui.createConfirmationPanel(g, nil, true, gui.Tr.SLocalize("ForcePush"), gui.Tr.SLocalize("ForcePushPrompt"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.pushWithForceFlag(g, v, true, "")
|
||||
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("ForcePush"), gui.Tr.SLocalize("ForcePushPrompt"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.pushWithForceFlag(g, v, true, "", "")
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error {
|
||||
file, err := gui.getSelectedFile(g)
|
||||
file, err := gui.getSelectedFile()
|
||||
if err != nil {
|
||||
if err != gui.Errors.ErrNoFiles {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !file.HasInlineMergeConflicts {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("FileNoMergeCons"))
|
||||
}
|
||||
if err := gui.changeMainViewsContext("merging"); err != nil {
|
||||
return err
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("FileNoMergeCons"))
|
||||
}
|
||||
gui.changeMainViewsContext("merging")
|
||||
if err := gui.switchFocus(g, v, gui.getMainView()); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.refreshMergePanel()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleAbortMerge(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.AbortMerge(); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
gui.createMessagePanel(g, v, "", gui.Tr.SLocalize("MergeAborted"))
|
||||
gui.refreshStatus(g)
|
||||
return gui.refreshFiles()
|
||||
}
|
||||
|
||||
func (gui *Gui) openFile(filename string) error {
|
||||
if err := gui.OSCommand.OpenFile(filename); err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -504,67 +589,6 @@ func (gui *Gui) anyFilesWithMergeConflicts() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type discardOption struct {
|
||||
handler func(fileName *commands.File) error
|
||||
description string
|
||||
}
|
||||
|
||||
// GetDisplayStrings is a function.
|
||||
func (r *discardOption) GetDisplayStrings(isFocused bool) []string {
|
||||
return []string{r.description}
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateDiscardMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
file, err := gui.getSelectedFile(g)
|
||||
if err != nil {
|
||||
if err != gui.Errors.ErrNoFiles {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
options := []*discardOption{
|
||||
{
|
||||
description: gui.Tr.SLocalize("discardAllChanges"),
|
||||
handler: func(file *commands.File) error {
|
||||
return gui.GitCommand.DiscardAllFileChanges(file)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: gui.Tr.SLocalize("cancel"),
|
||||
handler: func(file *commands.File) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if file.HasStagedChanges && file.HasUnstagedChanges {
|
||||
discardUnstagedChanges := &discardOption{
|
||||
description: gui.Tr.SLocalize("discardUnstagedChanges"),
|
||||
handler: func(file *commands.File) error {
|
||||
return gui.GitCommand.DiscardUnstagedFileChanges(file)
|
||||
},
|
||||
}
|
||||
|
||||
options = append(options[:1], append([]*discardOption{discardUnstagedChanges}, options[1:]...)...)
|
||||
}
|
||||
|
||||
handleMenuPress := func(index int) error {
|
||||
file, err := gui.getSelectedFile(g)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := options[index].handler(file); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.refreshFiles()
|
||||
}
|
||||
|
||||
return gui.createMenu(file.Name, options, len(options), handleMenuPress)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCustomCommand(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("CustomCommand"), "", func(g *gocui.Gui, v *gocui.View) error {
|
||||
command := gui.trimmedContent(v)
|
||||
@@ -573,45 +597,34 @@ func (gui *Gui) handleCustomCommand(g *gocui.Gui, v *gocui.View) error {
|
||||
})
|
||||
}
|
||||
|
||||
type stashOption struct {
|
||||
description string
|
||||
handler func() error
|
||||
}
|
||||
|
||||
// GetDisplayStrings is a function.
|
||||
func (o *stashOption) GetDisplayStrings(isFocused bool) []string {
|
||||
return []string{o.description}
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateStashMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
options := []*stashOption{
|
||||
menuItems := []*menuItem{
|
||||
{
|
||||
description: gui.Tr.SLocalize("stashAllChanges"),
|
||||
handler: func() error {
|
||||
displayString: gui.Tr.SLocalize("stashAllChanges"),
|
||||
onPress: func() error {
|
||||
return gui.handleStashSave(gui.GitCommand.StashSave)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: gui.Tr.SLocalize("stashStagedChanges"),
|
||||
handler: func() error {
|
||||
displayString: gui.Tr.SLocalize("stashStagedChanges"),
|
||||
onPress: func() error {
|
||||
return gui.handleStashSave(gui.GitCommand.StashSaveStagedChanges)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: gui.Tr.SLocalize("cancel"),
|
||||
handler: func() error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
handleMenuPress := func(index int) error {
|
||||
return options[index].handler()
|
||||
}
|
||||
|
||||
return gui.createMenu(gui.Tr.SLocalize("stashOptions"), options, len(options), handleMenuPress)
|
||||
return gui.createMenu(gui.Tr.SLocalize("stashOptions"), menuItems, createMenuOptions{showCancel: true})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStashChanges(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.handleStashSave(gui.GitCommand.StashSave)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateResetToUpstreamMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.createResetMenu("@{upstream}")
|
||||
}
|
||||
|
||||
func (gui *Gui) onFilesPanelSearchSelect(selectedLine int) error {
|
||||
gui.State.Panels.Files.SelectedLine = selectedLine
|
||||
return gui.focusAndSelectFile(gui.g, gui.getFilesView())
|
||||
}
|
||||
|
||||
21
pkg/gui/filtering.go
Normal file
21
pkg/gui/filtering.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package gui
|
||||
|
||||
import "github.com/jesseduffield/gocui"
|
||||
|
||||
func (gui *Gui) inFilterMode() bool {
|
||||
return gui.State.FilterPath != ""
|
||||
}
|
||||
|
||||
func (gui *Gui) validateNotInFilterMode() (bool, error) {
|
||||
if gui.inFilterMode() {
|
||||
return false, gui.createConfirmationPanel(gui.g, gui.g.CurrentView(), true, gui.Tr.SLocalize("MustExitFilterModeTitle"), gui.Tr.SLocalize("MustExitFilterModePrompt"), func(*gocui.Gui, *gocui.View) error {
|
||||
return gui.exitFilterMode()
|
||||
}, nil)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (gui *Gui) exitFilterMode() error {
|
||||
gui.State.FilterPath = ""
|
||||
return gui.Errors.ErrRestart
|
||||
}
|
||||
62
pkg/gui/filtering_menu_panel.go
Normal file
62
pkg/gui/filtering_menu_panel.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
func (gui *Gui) handleCreateFilteringMenuPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
fileName := ""
|
||||
switch v.Name() {
|
||||
case "files":
|
||||
file, err := gui.getSelectedFile()
|
||||
if err == nil {
|
||||
fileName = file.Name
|
||||
}
|
||||
case "commitFiles":
|
||||
file := gui.getSelectedCommitFile()
|
||||
if file != nil {
|
||||
fileName = file.Name
|
||||
}
|
||||
}
|
||||
|
||||
menuItems := []*menuItem{}
|
||||
|
||||
if fileName != "" {
|
||||
menuItems = append(menuItems, &menuItem{
|
||||
displayString: fmt.Sprintf("%s '%s'", gui.Tr.SLocalize("filterBy"), fileName),
|
||||
onPress: func() error {
|
||||
gui.State.FilterPath = fileName
|
||||
return gui.Errors.ErrRestart
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
menuItems = append(menuItems, &menuItem{
|
||||
displayString: gui.Tr.SLocalize("filterPathOption"),
|
||||
onPress: func() error {
|
||||
return gui.createPromptPanel(gui.g, v, gui.Tr.SLocalize("enterFileName"), "", func(g *gocui.Gui, promptView *gocui.View) error {
|
||||
gui.State.FilterPath = strings.TrimSpace(promptView.Buffer())
|
||||
return gui.Errors.ErrRestart
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
if gui.inFilterMode() {
|
||||
menuItems = append(menuItems, &menuItem{
|
||||
displayString: gui.Tr.SLocalize("exitFilterMode"),
|
||||
onPress: func() error {
|
||||
gui.State.FilterPath = ""
|
||||
return gui.Errors.ErrRestart
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return gui.createMenu(gui.Tr.SLocalize("FilteringMenuTitle"), menuItems, createMenuOptions{showCancel: true})
|
||||
}
|
||||
@@ -8,11 +8,6 @@ import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
type gitFlowOption struct {
|
||||
handler func() error
|
||||
description string
|
||||
}
|
||||
|
||||
func (gui *Gui) gitFlowFinishBranch(gitFlowConfig string, branchName string) error {
|
||||
// need to find out what kind of branch this is
|
||||
prefix := strings.SplitAfterN(branchName, "/", 2)[0]
|
||||
@@ -33,7 +28,7 @@ func (gui *Gui) gitFlowFinishBranch(gitFlowConfig string, branchName string) err
|
||||
}
|
||||
|
||||
if branchType == "" {
|
||||
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NotAGitFlowBranch"))
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("NotAGitFlowBranch"))
|
||||
}
|
||||
|
||||
subProcess := gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "finish", suffix)
|
||||
@@ -41,11 +36,6 @@ func (gui *Gui) gitFlowFinishBranch(gitFlowConfig string, branchName string) err
|
||||
return gui.Errors.ErrSubProcess
|
||||
}
|
||||
|
||||
// GetDisplayStrings is a function.
|
||||
func (r *gitFlowOption) GetDisplayStrings(isFocused bool) []string {
|
||||
return []string{r.description}
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateGitFlowMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
branch := gui.getSelectedBranch()
|
||||
if branch == nil {
|
||||
@@ -55,7 +45,7 @@ func (gui *Gui) handleCreateGitFlowMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
// get config
|
||||
gitFlowConfig, err := gui.OSCommand.RunCommandWithOutput("git config --local --get-regexp gitflow")
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(gui.g, "You need to install git-flow and enable it in this repo to use git-flow features")
|
||||
return gui.createErrorPanel("You need to install git-flow and enable it in this repo to use git-flow features")
|
||||
}
|
||||
|
||||
startHandler := func(branchType string) func() error {
|
||||
@@ -70,37 +60,31 @@ func (gui *Gui) handleCreateGitFlowMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
}
|
||||
|
||||
options := []*gitFlowOption{
|
||||
menuItems := []*menuItem{
|
||||
{
|
||||
// not localising here because it's one to one with the actual git flow commands
|
||||
description: fmt.Sprintf("finish branch '%s'", branch.Name),
|
||||
handler: func() error {
|
||||
displayString: fmt.Sprintf("finish branch '%s'", branch.Name),
|
||||
onPress: func() error {
|
||||
return gui.gitFlowFinishBranch(gitFlowConfig, branch.Name)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "start feature",
|
||||
handler: startHandler("feature"),
|
||||
displayString: "start feature",
|
||||
onPress: startHandler("feature"),
|
||||
},
|
||||
{
|
||||
description: "start hotfix",
|
||||
handler: startHandler("hotfix"),
|
||||
displayString: "start hotfix",
|
||||
onPress: startHandler("hotfix"),
|
||||
},
|
||||
{
|
||||
description: "start release",
|
||||
handler: startHandler("release"),
|
||||
displayString: "start bugfix",
|
||||
onPress: startHandler("bugfix"),
|
||||
},
|
||||
{
|
||||
description: gui.Tr.SLocalize("cancel"),
|
||||
handler: func() error {
|
||||
return nil
|
||||
},
|
||||
displayString: "start release",
|
||||
onPress: startHandler("release"),
|
||||
},
|
||||
}
|
||||
|
||||
handleMenuPress := func(index int) error {
|
||||
return options[index].handler()
|
||||
}
|
||||
|
||||
return gui.createMenu("git flow", options, len(options), handleMenuPress)
|
||||
return gui.createMenu("git flow", menuItems, createMenuOptions{})
|
||||
}
|
||||
|
||||
190
pkg/gui/global_handlers.go
Normal file
190
pkg/gui/global_handlers.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
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)
|
||||
// commits render differently depending on whether we're in fullscreen more or not
|
||||
if err := gui.refreshCommitsViewWithSelection(); err != nil {
|
||||
return err
|
||||
}
|
||||
// same with branches
|
||||
if err := gui.refreshBranchesViewWithSelection(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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)
|
||||
// commits render differently depending on whether we're in fullscreen more or not
|
||||
if err := gui.refreshCommitsViewWithSelection(); err != nil {
|
||||
return err
|
||||
}
|
||||
// same with branches
|
||||
if err := gui.refreshBranchesViewWithSelection(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollUpView(viewName string) error {
|
||||
mainView, _ := gui.g.View(viewName)
|
||||
ox, oy := mainView.Origin()
|
||||
newOy := int(math.Max(0, float64(oy-gui.Config.GetUserConfig().GetInt("gui.scrollHeight"))))
|
||||
return mainView.SetOrigin(ox, newOy)
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollDownView(viewName string) error {
|
||||
mainView, _ := gui.g.View(viewName)
|
||||
ox, oy := mainView.Origin()
|
||||
y := oy
|
||||
if !gui.Config.GetUserConfig().GetBool("gui.scrollPastBottom") {
|
||||
_, sy := mainView.Size()
|
||||
y += sy
|
||||
}
|
||||
scrollHeight := gui.Config.GetUserConfig().GetInt("gui.scrollHeight")
|
||||
if y < mainView.LinesHeight() {
|
||||
if err := mainView.SetOrigin(ox, oy+scrollHeight); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if manager, ok := gui.viewBufferManagerMap[viewName]; ok {
|
||||
manager.ReadLines(scrollHeight)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollUpMain(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.canScrollMergePanel() {
|
||||
gui.State.Panels.Merging.UserScrolling = true
|
||||
}
|
||||
|
||||
return gui.scrollUpView("main")
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollDownMain(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.canScrollMergePanel() {
|
||||
gui.State.Panels.Merging.UserScrolling = true
|
||||
}
|
||||
|
||||
return gui.scrollDownView("main")
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollUpSecondary(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.scrollUpView("secondary")
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollDownSecondary(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.scrollDownView("secondary")
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollUpConfirmationPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
if v.Editable {
|
||||
return nil
|
||||
}
|
||||
return gui.scrollUpView("confirmation")
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollDownConfirmationPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
if v.Editable {
|
||||
return nil
|
||||
}
|
||||
return gui.scrollDownView("confirmation")
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRefresh(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMouseDownMain(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch g.CurrentView().Name() {
|
||||
case "files":
|
||||
return gui.enterFile(false, v.SelectedLineIdx())
|
||||
case "commitFiles":
|
||||
return gui.enterCommitFile(v.SelectedLineIdx())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMouseDownSecondary(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch g.CurrentView().Name() {
|
||||
case "files":
|
||||
return gui.enterFile(true, v.SelectedLineIdx())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleInfoClick(g *gocui.Gui, v *gocui.View) error {
|
||||
if !gui.g.Mouse {
|
||||
return nil
|
||||
}
|
||||
|
||||
cx, _ := v.Cursor()
|
||||
width, _ := v.Size()
|
||||
|
||||
// if we're in the normal context there will be a donate button here
|
||||
// if we have ('reset') at the end then
|
||||
if gui.inFilterMode() {
|
||||
if width-cx <= len(gui.Tr.SLocalize("(reset)")) {
|
||||
return gui.exitFilterMode()
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if gui.inDiffMode() {
|
||||
if width-cx <= len(gui.Tr.SLocalize("(reset)")) {
|
||||
return gui.exitDiffMode()
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if cx <= len(gui.Tr.SLocalize("Donate")) {
|
||||
return gui.OSCommand.OpenLink("https://github.com/sponsors/jesseduffield")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) fetch(canPromptForCredentials bool) (err error) {
|
||||
fetchOpts := commands.FetchOptions{}
|
||||
if canPromptForCredentials {
|
||||
fetchOpts.PromptUserForCredential = gui.promptUserForCredential
|
||||
}
|
||||
|
||||
err = gui.GitCommand.Fetch(fetchOpts)
|
||||
|
||||
if canPromptForCredentials && err != nil && strings.Contains(err.Error(), "exit status 128") {
|
||||
colorFunction := color.New(color.FgRed).SprintFunc()
|
||||
coloredMessage := colorFunction(strings.TrimSpace(gui.Tr.SLocalize("PassUnameWrong")))
|
||||
close := func(g *gocui.Gui, v *gocui.View) error {
|
||||
return nil
|
||||
}
|
||||
_ = gui.createConfirmationPanel(gui.g, gui.g.CurrentView(), true, gui.Tr.SLocalize("Error"), coloredMessage, close, close)
|
||||
}
|
||||
|
||||
gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, COMMITS, REMOTES, TAGS}, mode: ASYNC})
|
||||
|
||||
return err
|
||||
}
|
||||
880
pkg/gui/gui.go
880
pkg/gui/gui.go
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
524
pkg/gui/layout.go
Normal file
524
pkg/gui/layout.go
Normal file
@@ -0,0 +1,524 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// getFocusLayout returns a manager function for when view gain and lose focus
|
||||
func (gui *Gui) getFocusLayout() func(g *gocui.Gui) error {
|
||||
var previousView *gocui.View
|
||||
return func(g *gocui.Gui) error {
|
||||
newView := gui.g.CurrentView()
|
||||
if err := gui.onFocusChange(); err != nil {
|
||||
return err
|
||||
}
|
||||
// for now we don't consider losing focus to a popup panel as actually losing focus
|
||||
if newView != previousView && !gui.isPopupPanel(newView.Name()) {
|
||||
if err := gui.onFocusLost(previousView, newView); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.onFocus(newView); err != nil {
|
||||
return err
|
||||
}
|
||||
previousView = newView
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) onFocusChange() error {
|
||||
currentView := gui.g.CurrentView()
|
||||
for _, view := range gui.g.Views() {
|
||||
view.Highlight = view == currentView
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) onFocusLost(v *gocui.View, newView *gocui.View) error {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
if v.IsSearching() && newView.Name() != "search" {
|
||||
if err := gui.onSearchEscape(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
switch v.Name() {
|
||||
case "main":
|
||||
// if we have lost focus to a first-class panel, we need to do some cleanup
|
||||
gui.changeMainViewsContext("normal")
|
||||
case "commitFiles":
|
||||
if gui.State.MainContext != "patch-building" {
|
||||
if _, err := gui.g.SetViewOnBottom(v.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
gui.Log.Info(v.Name() + " focus lost")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) onFocus(v *gocui.View) error {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
gui.Log.Info(v.Name() + " focus gained")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) getViewHeights() map[string]int {
|
||||
currView := gui.g.CurrentView()
|
||||
currentCyclebleView := gui.State.PreviousView
|
||||
if currView != nil {
|
||||
viewName := currView.Name()
|
||||
usePreviousView := true
|
||||
for _, view := range cyclableViews {
|
||||
if view == viewName {
|
||||
currentCyclebleView = viewName
|
||||
usePreviousView = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if usePreviousView {
|
||||
currentCyclebleView = gui.State.PreviousView
|
||||
}
|
||||
}
|
||||
|
||||
// unfortunate result of the fact that these are separate views, have to map explicitly
|
||||
if currentCyclebleView == "commitFiles" {
|
||||
currentCyclebleView = "commits"
|
||||
}
|
||||
|
||||
_, height := gui.g.Size()
|
||||
|
||||
if gui.State.ScreenMode == SCREEN_FULL || gui.State.ScreenMode == SCREEN_HALF {
|
||||
vHeights := map[string]int{
|
||||
"status": 0,
|
||||
"files": 0,
|
||||
"branches": 0,
|
||||
"commits": 0,
|
||||
"stash": 0,
|
||||
"options": 0,
|
||||
}
|
||||
vHeights[currentCyclebleView] = height - 1
|
||||
return vHeights
|
||||
}
|
||||
|
||||
usableSpace := height - 7
|
||||
extraSpace := usableSpace - (usableSpace/3)*3
|
||||
|
||||
if height >= 28 {
|
||||
return map[string]int{
|
||||
"status": 3,
|
||||
"files": (usableSpace / 3) + extraSpace,
|
||||
"branches": usableSpace / 3,
|
||||
"commits": usableSpace / 3,
|
||||
"stash": 3,
|
||||
"options": 1,
|
||||
}
|
||||
}
|
||||
|
||||
defaultHeight := 3
|
||||
if height < 21 {
|
||||
defaultHeight = 1
|
||||
}
|
||||
vHeights := map[string]int{
|
||||
"status": defaultHeight,
|
||||
"files": defaultHeight,
|
||||
"branches": defaultHeight,
|
||||
"commits": defaultHeight,
|
||||
"stash": defaultHeight,
|
||||
"options": defaultHeight,
|
||||
}
|
||||
vHeights[currentCyclebleView] = height - defaultHeight*4 - 1
|
||||
|
||||
return vHeights
|
||||
}
|
||||
|
||||
// layout is called for every screen re-render e.g. when the screen is resized
|
||||
func (gui *Gui) layout(g *gocui.Gui) error {
|
||||
g.Highlight = true
|
||||
width, height := g.Size()
|
||||
|
||||
information := gui.Config.GetVersion()
|
||||
if gui.g.Mouse {
|
||||
donate := color.New(color.FgMagenta, color.Underline).Sprint(gui.Tr.SLocalize("Donate"))
|
||||
information = donate + " " + information
|
||||
}
|
||||
if gui.inDiffMode() {
|
||||
information = utils.ColoredString(fmt.Sprintf("%s %s %s", gui.Tr.SLocalize("showingGitDiff"), "git diff "+gui.diffStr(), utils.ColoredString(gui.Tr.SLocalize("(reset)"), color.Underline)), color.FgMagenta)
|
||||
} else if gui.inFilterMode() {
|
||||
information = utils.ColoredString(fmt.Sprintf("%s '%s' %s", gui.Tr.SLocalize("filteringBy"), gui.State.FilterPath, utils.ColoredString(gui.Tr.SLocalize("(reset)"), color.Underline)), color.FgRed, color.Bold)
|
||||
} else if len(gui.State.CherryPickedCommits) > 0 {
|
||||
information = utils.ColoredString(fmt.Sprintf("%d commits copied", len(gui.State.CherryPickedCommits)), color.FgCyan)
|
||||
}
|
||||
|
||||
minimumHeight := 9
|
||||
minimumWidth := 10
|
||||
if height < minimumHeight || width < minimumWidth {
|
||||
v, err := g.SetView("limit", 0, 0, width-1, height-1, 0)
|
||||
if err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
v.Title = gui.Tr.SLocalize("NotEnoughSpace")
|
||||
v.Wrap = true
|
||||
_, _ = g.SetViewOnTop("limit")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
vHeights := gui.getViewHeights()
|
||||
|
||||
optionsVersionBoundary := width - max(len(utils.Decolorise(information)), 1)
|
||||
|
||||
appStatus := gui.statusManager.getStatusString()
|
||||
appStatusOptionsBoundary := 0
|
||||
if appStatus != "" {
|
||||
appStatusOptionsBoundary = len(appStatus) + 2
|
||||
}
|
||||
|
||||
_, _ = g.SetViewOnBottom("limit")
|
||||
_ = g.DeleteView("limit")
|
||||
|
||||
sidePanelWidthRatio := gui.Config.GetUserConfig().GetFloat64("gui.sidePanelWidth")
|
||||
|
||||
textColor := theme.GocuiDefaultTextColor
|
||||
var leftSideWidth int
|
||||
switch gui.State.ScreenMode {
|
||||
case SCREEN_NORMAL:
|
||||
leftSideWidth = int(float64(width) * sidePanelWidthRatio)
|
||||
case SCREEN_HALF:
|
||||
leftSideWidth = width/2 - 2
|
||||
case SCREEN_FULL:
|
||||
currentView := gui.g.CurrentView()
|
||||
if currentView != nil && currentView.Name() == "main" {
|
||||
leftSideWidth = 0
|
||||
} else {
|
||||
leftSideWidth = width - 1
|
||||
}
|
||||
}
|
||||
|
||||
mainPanelLeft := leftSideWidth + 1
|
||||
mainPanelRight := width - 1
|
||||
secondaryPanelLeft := width - 1
|
||||
secondaryPanelTop := 0
|
||||
mainPanelBottom := height - 2
|
||||
if gui.State.SplitMainPanel {
|
||||
if gui.State.ScreenMode == SCREEN_FULL {
|
||||
mainPanelLeft = 0
|
||||
panelSplitX := width/2 - 4
|
||||
mainPanelRight = panelSplitX
|
||||
secondaryPanelLeft = panelSplitX + 1
|
||||
} else if width < 220 {
|
||||
mainPanelBottom = height/2 - 1
|
||||
secondaryPanelTop = mainPanelBottom + 1
|
||||
secondaryPanelLeft = leftSideWidth + 1
|
||||
} else {
|
||||
units := 5
|
||||
leftSideWidth = width / units
|
||||
mainPanelLeft = leftSideWidth + 1
|
||||
panelSplitX := (1 + ((units - 1) / 2)) * width / units
|
||||
mainPanelRight = panelSplitX
|
||||
secondaryPanelLeft = panelSplitX + 1
|
||||
}
|
||||
}
|
||||
|
||||
main := "main"
|
||||
secondary := "secondary"
|
||||
swappingMainPanels := gui.State.Panels.LineByLine != nil && gui.State.Panels.LineByLine.SecondaryFocused
|
||||
if swappingMainPanels {
|
||||
main = "secondary"
|
||||
secondary = "main"
|
||||
}
|
||||
|
||||
// reading more lines into main view buffers upon resize
|
||||
prevMainView, err := gui.g.View("main")
|
||||
if err == nil {
|
||||
_, prevMainHeight := prevMainView.Size()
|
||||
heightDiff := mainPanelBottom - prevMainHeight - 1
|
||||
if heightDiff > 0 {
|
||||
if manager, ok := gui.viewBufferManagerMap["main"]; ok {
|
||||
manager.ReadLines(heightDiff)
|
||||
}
|
||||
if manager, ok := gui.viewBufferManagerMap["secondary"]; ok {
|
||||
manager.ReadLines(heightDiff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
v, err := g.SetView(main, mainPanelLeft, 0, mainPanelRight, mainPanelBottom, gocui.LEFT)
|
||||
if err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
v.Title = gui.Tr.SLocalize("DiffTitle")
|
||||
v.Wrap = true
|
||||
v.FgColor = textColor
|
||||
v.IgnoreCarriageReturns = true
|
||||
}
|
||||
|
||||
hiddenViewOffset := 9999
|
||||
|
||||
hiddenSecondaryPanelOffset := 0
|
||||
if !gui.State.SplitMainPanel {
|
||||
hiddenSecondaryPanelOffset = hiddenViewOffset
|
||||
}
|
||||
secondaryView, err := g.SetView(secondary, secondaryPanelLeft+hiddenSecondaryPanelOffset, hiddenSecondaryPanelOffset+secondaryPanelTop, width-1+hiddenSecondaryPanelOffset, height-2+hiddenSecondaryPanelOffset, gocui.LEFT)
|
||||
if err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
secondaryView.Title = gui.Tr.SLocalize("DiffTitle")
|
||||
secondaryView.Wrap = true
|
||||
secondaryView.FgColor = gocui.ColorWhite
|
||||
secondaryView.IgnoreCarriageReturns = true
|
||||
}
|
||||
|
||||
if v, err := g.SetView("status", 0, 0, leftSideWidth, vHeights["status"]-1, gocui.BOTTOM|gocui.RIGHT); err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
v.Title = gui.Tr.SLocalize("StatusTitle")
|
||||
v.FgColor = textColor
|
||||
}
|
||||
|
||||
filesView, err := g.SetViewBeneath("files", "status", vHeights["files"])
|
||||
if err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
filesView.Highlight = true
|
||||
filesView.Title = gui.Tr.SLocalize("FilesTitle")
|
||||
filesView.SetOnSelectItem(gui.onSelectItemWrapper(gui.onFilesPanelSearchSelect))
|
||||
filesView.ContainsList = true
|
||||
}
|
||||
|
||||
branchesView, err := g.SetViewBeneath("branches", "files", vHeights["branches"])
|
||||
if err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
branchesView.Title = gui.Tr.SLocalize("BranchesTitle")
|
||||
branchesView.Tabs = []string{"Local Branches", "Remotes", "Tags"}
|
||||
branchesView.FgColor = textColor
|
||||
branchesView.SetOnSelectItem(gui.onSelectItemWrapper(gui.onBranchesPanelSearchSelect))
|
||||
branchesView.ContainsList = true
|
||||
}
|
||||
|
||||
commitFilesView, err := g.SetViewBeneath("commitFiles", "branches", vHeights["commits"])
|
||||
if err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
commitFilesView.Title = gui.Tr.SLocalize("CommitFiles")
|
||||
commitFilesView.FgColor = textColor
|
||||
commitFilesView.SetOnSelectItem(gui.onSelectItemWrapper(gui.onCommitFilesPanelSearchSelect))
|
||||
commitFilesView.ContainsList = true
|
||||
}
|
||||
|
||||
commitsView, err := g.SetViewBeneath("commits", "branches", vHeights["commits"])
|
||||
if err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
commitsView.Title = gui.Tr.SLocalize("CommitsTitle")
|
||||
commitsView.Tabs = []string{"Commits", "Reflog"}
|
||||
commitsView.FgColor = textColor
|
||||
commitsView.SetOnSelectItem(gui.onSelectItemWrapper(gui.onCommitsPanelSearchSelect))
|
||||
commitsView.ContainsList = true
|
||||
}
|
||||
|
||||
stashView, err := g.SetViewBeneath("stash", "commits", vHeights["stash"])
|
||||
if err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
stashView.Title = gui.Tr.SLocalize("StashTitle")
|
||||
stashView.FgColor = textColor
|
||||
stashView.SetOnSelectItem(gui.onSelectItemWrapper(gui.onStashPanelSearchSelect))
|
||||
stashView.ContainsList = true
|
||||
}
|
||||
|
||||
if v, err := g.SetView("options", appStatusOptionsBoundary-1, height-2, optionsVersionBoundary-1, height, 0); err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
v.Frame = false
|
||||
v.FgColor = theme.OptionsColor
|
||||
}
|
||||
|
||||
if gui.getCommitMessageView() == nil {
|
||||
// doesn't matter where this view starts because it will be hidden
|
||||
if commitMessageView, err := g.SetView("commitMessage", hiddenViewOffset, hiddenViewOffset, hiddenViewOffset+10, hiddenViewOffset+10, 0); err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
_, _ = g.SetViewOnBottom("commitMessage")
|
||||
commitMessageView.Title = gui.Tr.SLocalize("CommitMessage")
|
||||
commitMessageView.FgColor = textColor
|
||||
commitMessageView.Editable = true
|
||||
commitMessageView.Editor = gocui.EditorFunc(gui.commitMessageEditor)
|
||||
}
|
||||
}
|
||||
|
||||
if check, _ := g.View("credentials"); check == nil {
|
||||
// doesn't matter where this view starts because it will be hidden
|
||||
if credentialsView, err := g.SetView("credentials", hiddenViewOffset, hiddenViewOffset, hiddenViewOffset+10, hiddenViewOffset+10, 0); err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
_, err := g.SetViewOnBottom("credentials")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
credentialsView.Title = gui.Tr.SLocalize("CredentialsUsername")
|
||||
credentialsView.FgColor = textColor
|
||||
credentialsView.Editable = true
|
||||
}
|
||||
}
|
||||
|
||||
searchViewOffset := hiddenViewOffset
|
||||
if gui.State.Searching.isSearching {
|
||||
searchViewOffset = 0
|
||||
}
|
||||
|
||||
// this view takes up one character. Its only purpose is to show the slash when searching
|
||||
searchPrefix := "search: "
|
||||
if searchPrefixView, err := g.SetView("searchPrefix", appStatusOptionsBoundary-1+searchViewOffset, height-2+searchViewOffset, len(searchPrefix)+searchViewOffset, height+searchViewOffset, 0); err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
|
||||
searchPrefixView.BgColor = gocui.ColorDefault
|
||||
searchPrefixView.FgColor = gocui.ColorGreen
|
||||
searchPrefixView.Frame = false
|
||||
gui.setViewContent(searchPrefixView, searchPrefix)
|
||||
}
|
||||
|
||||
if searchView, err := g.SetView("search", appStatusOptionsBoundary-1+searchViewOffset+len(searchPrefix), height-2+searchViewOffset, optionsVersionBoundary+searchViewOffset, height+searchViewOffset, 0); err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
|
||||
searchView.BgColor = gocui.ColorDefault
|
||||
searchView.FgColor = gocui.ColorGreen
|
||||
searchView.Frame = false
|
||||
searchView.Editable = true
|
||||
}
|
||||
|
||||
if appStatusView, err := g.SetView("appStatus", -1, height-2, width, height, 0); err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
appStatusView.BgColor = gocui.ColorDefault
|
||||
appStatusView.FgColor = gocui.ColorCyan
|
||||
appStatusView.Frame = false
|
||||
if _, err := g.SetViewOnBottom("appStatus"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
informationView, err := g.SetView("information", optionsVersionBoundary-1, height-2, width, height, 0)
|
||||
if err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
informationView.BgColor = gocui.ColorDefault
|
||||
informationView.FgColor = gocui.ColorGreen
|
||||
informationView.Frame = false
|
||||
gui.renderString(g, "information", information)
|
||||
|
||||
// doing this here because it'll only happen once
|
||||
if err := gui.onInitialViewsCreation(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if gui.State.OldInformation != information {
|
||||
gui.setViewContent(informationView, information)
|
||||
gui.State.OldInformation = information
|
||||
}
|
||||
|
||||
if gui.g.CurrentView() == nil {
|
||||
initialView := gui.getFilesView()
|
||||
if gui.inFilterMode() {
|
||||
initialView = gui.getCommitsView()
|
||||
}
|
||||
if _, err := gui.g.SetCurrentView(initialView.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.switchFocus(gui.g, nil, initialView); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
type listViewState struct {
|
||||
selectedLine int
|
||||
lineCount int
|
||||
view *gocui.View
|
||||
context string
|
||||
}
|
||||
|
||||
listViews := []listViewState{
|
||||
{view: filesView, context: "", selectedLine: gui.State.Panels.Files.SelectedLine, lineCount: len(gui.State.Files)},
|
||||
{view: branchesView, context: "local-branches", selectedLine: gui.State.Panels.Branches.SelectedLine, lineCount: len(gui.State.Branches)},
|
||||
{view: branchesView, context: "remotes", selectedLine: gui.State.Panels.Remotes.SelectedLine, lineCount: len(gui.State.Remotes)},
|
||||
{view: branchesView, context: "remote-branches", selectedLine: gui.State.Panels.RemoteBranches.SelectedLine, lineCount: len(gui.State.Remotes)},
|
||||
{view: commitsView, context: "branch-commits", selectedLine: gui.State.Panels.Commits.SelectedLine, lineCount: len(gui.State.Commits)},
|
||||
{view: commitsView, context: "reflog-commits", selectedLine: gui.State.Panels.ReflogCommits.SelectedLine, lineCount: len(gui.State.FilteredReflogCommits)},
|
||||
{view: stashView, context: "", selectedLine: gui.State.Panels.Stash.SelectedLine, lineCount: len(gui.State.StashEntries)},
|
||||
{view: commitFilesView, context: "", selectedLine: gui.State.Panels.CommitFiles.SelectedLine, lineCount: len(gui.State.CommitFiles)},
|
||||
}
|
||||
|
||||
// menu view might not exist so we check to be safe
|
||||
if menuView, err := gui.g.View("menu"); err == nil {
|
||||
listViews = append(listViews, listViewState{view: menuView, context: "", selectedLine: gui.State.Panels.Menu.SelectedLine, lineCount: gui.State.MenuItemCount})
|
||||
}
|
||||
for _, listView := range listViews {
|
||||
// ignore views where the context doesn't match up with the selected line we're trying to focus
|
||||
if listView.context != "" && (listView.view.Context != listView.context) {
|
||||
continue
|
||||
}
|
||||
// check if the selected line is now out of view and if so refocus it
|
||||
listView.view.FocusPoint(0, listView.selectedLine)
|
||||
|
||||
listView.view.SelBgColor = theme.GocuiSelectedLineBgColor
|
||||
}
|
||||
|
||||
mainViewWidth, mainViewHeight := gui.getMainView().Size()
|
||||
if mainViewWidth != gui.State.PrevMainWidth || mainViewHeight != gui.State.PrevMainHeight {
|
||||
gui.State.PrevMainWidth = mainViewWidth
|
||||
gui.State.PrevMainHeight = mainViewHeight
|
||||
if err := gui.onResize(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// here is a good place log some stuff
|
||||
// if you download humanlog and do tail -f development.log | humanlog
|
||||
// this will let you see these branches as prettified json
|
||||
// gui.Log.Info(utils.AsJson(gui.State.Branches[0:4]))
|
||||
return gui.resizeCurrentPopupPanel(g)
|
||||
}
|
||||
|
||||
func (gui *Gui) onInitialViewsCreation() error {
|
||||
gui.changeMainViewsContext("normal")
|
||||
|
||||
gui.getBranchesView().Context = "local-branches"
|
||||
gui.getCommitsView().Context = "branch-commits"
|
||||
|
||||
return gui.loadNewRepo()
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -88,7 +88,8 @@ func (gui *Gui) refreshLineByLinePanel(diff string, secondaryDiff string, second
|
||||
}
|
||||
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
return gui.setViewContent(gui.g, gui.getSecondaryView(), secondaryPatchParser.Render(-1, -1, nil))
|
||||
gui.setViewContent(gui.getSecondaryView(), secondaryPatchParser.Render(-1, -1, nil))
|
||||
return nil
|
||||
})
|
||||
|
||||
return false, nil
|
||||
@@ -220,6 +221,10 @@ func (gui *Gui) handleMouseScrollDown(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.handleCycleLine(1)
|
||||
}
|
||||
|
||||
func (gui *Gui) getSelectedCommitFileName() string {
|
||||
return gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLine].Name
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshMainView() error {
|
||||
state := gui.State.Panels.LineByLine
|
||||
|
||||
@@ -227,7 +232,7 @@ func (gui *Gui) refreshMainView() error {
|
||||
// I'd prefer not to have knowledge of contexts using this file but I'm not sure
|
||||
// how to get around this
|
||||
if gui.State.MainContext == "patch-building" {
|
||||
filename := gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLine].Name
|
||||
filename := gui.getSelectedCommitFileName()
|
||||
includedLineIndices = gui.GitCommand.PatchManager.GetFileIncLineIndices(filename)
|
||||
}
|
||||
colorDiff := state.PatchParser.Render(state.FirstLineIdx, state.LastLineIdx, includedLineIndices)
|
||||
@@ -237,7 +242,8 @@ func (gui *Gui) refreshMainView() error {
|
||||
mainView.Wrap = false
|
||||
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
return gui.setViewContent(gui.g, gui.getMainView(), colorDiff)
|
||||
gui.setViewContent(gui.getMainView(), colorDiff)
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
@@ -314,3 +320,8 @@ func (gui *Gui) handleToggleSelectHunk(g *gocui.Gui, v *gocui.View) error {
|
||||
|
||||
return gui.focusSelection(state.SelectMode == HUNK)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleEscapeLineByLinePanel() {
|
||||
gui.changeMainViewsContext("normal")
|
||||
gui.State.Panels.LineByLine = nil
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ func (lv *listView) handleLineChange(change int) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
view, err := lv.gui.g.View(lv.viewName)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -41,6 +42,40 @@ func (lv *listView) handleLineChange(change int) error {
|
||||
return lv.handleItemSelect(lv.gui.g, view)
|
||||
}
|
||||
|
||||
func (lv *listView) handleNextPage(g *gocui.Gui, v *gocui.View) error {
|
||||
view, err := lv.gui.g.View(lv.viewName)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
_, height := view.Size()
|
||||
delta := height - 1
|
||||
if delta == 0 {
|
||||
delta = 1
|
||||
}
|
||||
return lv.handleLineChange(delta)
|
||||
}
|
||||
|
||||
func (lv *listView) handleGotoTop(g *gocui.Gui, v *gocui.View) error {
|
||||
return lv.handleLineChange(-lv.getItemsLength())
|
||||
}
|
||||
|
||||
func (lv *listView) handleGotoBottom(g *gocui.Gui, v *gocui.View) error {
|
||||
return lv.handleLineChange(lv.getItemsLength())
|
||||
}
|
||||
|
||||
func (lv *listView) handlePrevPage(g *gocui.Gui, v *gocui.View) error {
|
||||
view, err := lv.gui.g.View(lv.viewName)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
_, height := view.Size()
|
||||
delta := height - 1
|
||||
if delta == 0 {
|
||||
delta = 1
|
||||
}
|
||||
return lv.handleLineChange(-delta)
|
||||
}
|
||||
|
||||
func (lv *listView) handleClick(g *gocui.Gui, v *gocui.View) error {
|
||||
if !lv.gui.isPopupPanel(lv.viewName) && lv.gui.popupPanelFocused() {
|
||||
return nil
|
||||
@@ -56,7 +91,14 @@ func (lv *listView) handleClick(g *gocui.Gui, v *gocui.View) error {
|
||||
|
||||
*selectedLineIdxPtr = newSelectedLineIdx
|
||||
|
||||
if prevSelectedLineIdx == newSelectedLineIdx && lv.gui.currentViewName() == lv.viewName && lv.handleClickSelectedItem != nil {
|
||||
if lv.rendersToMainView {
|
||||
if err := lv.gui.resetOrigin(lv.gui.getMainView()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
prevViewName := lv.gui.currentViewName()
|
||||
if prevSelectedLineIdx == newSelectedLineIdx && prevViewName == lv.viewName && lv.handleClickSelectedItem != nil {
|
||||
return lv.handleClickSelectedItem(lv.gui.g, v)
|
||||
}
|
||||
return lv.handleItemSelect(lv.gui.g, v)
|
||||
@@ -141,7 +183,7 @@ func (gui *Gui) getListViews() []*listView {
|
||||
{
|
||||
viewName: "commits",
|
||||
context: "reflog-commits",
|
||||
getItemsLength: func() int { return len(gui.State.ReflogCommits) },
|
||||
getItemsLength: func() int { return len(gui.State.FilteredReflogCommits) },
|
||||
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.ReflogCommits.SelectedLine },
|
||||
handleFocus: gui.handleReflogCommitSelect,
|
||||
handleItemSelect: gui.handleReflogCommitSelect,
|
||||
|
||||
@@ -8,17 +8,24 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type menuItem struct {
|
||||
displayString string
|
||||
displayStrings []string
|
||||
onPress func() error
|
||||
}
|
||||
|
||||
// list panel functions
|
||||
|
||||
func (gui *Gui) handleMenuSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.focusPoint(0, gui.State.Panels.Menu.SelectedLine, gui.State.MenuItemCount, v)
|
||||
v.FocusPoint(0, gui.State.Panels.Menu.SelectedLine)
|
||||
return nil
|
||||
}
|
||||
|
||||
// specific functions
|
||||
|
||||
func (gui *Gui) renderMenuOptions() error {
|
||||
optionsMap := map[string]string{
|
||||
fmt.Sprintf("%s/%s", gui.getKeyDisplay("universal.return"), gui.getKeyDisplay("universal.quit")): gui.Tr.SLocalize("close"),
|
||||
gui.getKeyDisplay("universal.return"): gui.Tr.SLocalize("close"),
|
||||
fmt.Sprintf("%s %s", gui.getKeyDisplay("universal.prevItem"), gui.getKeyDisplay("universal.nextItem")): gui.Tr.SLocalize("navigate"),
|
||||
gui.getKeyDisplay("universal.select"): gui.Tr.SLocalize("execute"),
|
||||
}
|
||||
@@ -38,27 +45,54 @@ func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.returnFocus(g, v)
|
||||
}
|
||||
|
||||
func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handlePress func(int) error) error {
|
||||
isFocused := gui.g.CurrentView().Name() == "menu"
|
||||
gui.State.MenuItemCount = itemCount
|
||||
list, err := utils.RenderList(items, isFocused)
|
||||
if err != nil {
|
||||
return err
|
||||
type createMenuOptions struct {
|
||||
showCancel bool
|
||||
}
|
||||
|
||||
func (gui *Gui) createMenu(title string, items []*menuItem, createMenuOptions createMenuOptions) error {
|
||||
if createMenuOptions.showCancel {
|
||||
// this is mutative but I'm okay with that for now
|
||||
items = append(items, &menuItem{
|
||||
displayStrings: []string{gui.Tr.SLocalize("cancel")},
|
||||
onPress: func() error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
gui.State.MenuItemCount = len(items)
|
||||
|
||||
stringArrays := make([][]string, len(items))
|
||||
for i, item := range items {
|
||||
if item.displayStrings == nil {
|
||||
stringArrays[i] = []string{item.displayString}
|
||||
} else {
|
||||
stringArrays[i] = item.displayStrings
|
||||
}
|
||||
}
|
||||
|
||||
list := utils.RenderDisplayStrings(stringArrays)
|
||||
|
||||
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, false, list)
|
||||
menuView, _ := gui.g.SetView("menu", x0, y0, x1, y1, 0)
|
||||
menuView.Title = title
|
||||
menuView.FgColor = theme.GocuiDefaultTextColor
|
||||
menuView.ContainsList = true
|
||||
menuView.Clear()
|
||||
menuView.SetOnSelectItem(gui.onSelectItemWrapper(func(selectedLine int) error {
|
||||
gui.State.Panels.Menu.SelectedLine = selectedLine
|
||||
menuView.FocusPoint(0, selectedLine)
|
||||
return nil
|
||||
}))
|
||||
fmt.Fprint(menuView, list)
|
||||
gui.State.Panels.Menu.SelectedLine = 0
|
||||
|
||||
wrappedHandlePress := func(g *gocui.Gui, v *gocui.View) error {
|
||||
selectedLine := gui.State.Panels.Menu.SelectedLine
|
||||
if err := handlePress(selectedLine); err != nil {
|
||||
if err := items[selectedLine].onPress(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := gui.g.View("menu"); err == nil {
|
||||
if _, err := gui.g.SetViewOnBottom("menu"); err != nil {
|
||||
return err
|
||||
|
||||
@@ -59,6 +59,7 @@ func (gui *Gui) coloredConflictFile(content string, conflicts []commands.Conflic
|
||||
colour := color.New(colourAttr)
|
||||
if hasFocus && conflictIndex < len(conflicts) && conflicts[conflictIndex] == conflict && gui.shouldHighlightLine(i, conflict, conflictTop) {
|
||||
colour.Add(color.Bold)
|
||||
colour.Add(theme.SelectedRangeBgColor)
|
||||
}
|
||||
if i == conflict.End && len(remainingConflicts) > 0 {
|
||||
conflict, remainingConflicts = gui.shiftConflict(remainingConflicts)
|
||||
@@ -68,17 +69,24 @@ func (gui *Gui) coloredConflictFile(content string, conflicts []commands.Conflic
|
||||
return outputBuffer.String(), nil
|
||||
}
|
||||
|
||||
func (gui *Gui) takeOverScrolling() {
|
||||
gui.State.Panels.Merging.UserScrolling = false
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSelectTop(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.takeOverScrolling()
|
||||
gui.State.Panels.Merging.ConflictTop = true
|
||||
return gui.refreshMergePanel()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSelectBottom(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.takeOverScrolling()
|
||||
gui.State.Panels.Merging.ConflictTop = false
|
||||
return gui.refreshMergePanel()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSelectNextConflict(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.takeOverScrolling()
|
||||
if gui.State.Panels.Merging.ConflictIndex >= len(gui.State.Panels.Merging.Conflicts)-1 {
|
||||
return nil
|
||||
}
|
||||
@@ -87,6 +95,7 @@ func (gui *Gui) handleSelectNextConflict(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSelectPrevConflict(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.takeOverScrolling()
|
||||
if gui.State.Panels.Merging.ConflictIndex <= 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -104,7 +113,7 @@ func (gui *Gui) isIndexToDelete(i int, conflict commands.Conflict, pick string)
|
||||
}
|
||||
|
||||
func (gui *Gui) resolveConflict(g *gocui.Gui, conflict commands.Conflict, pick string) error {
|
||||
gitFile, err := gui.getSelectedFile(g)
|
||||
gitFile, err := gui.getSelectedFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -130,7 +139,7 @@ func (gui *Gui) resolveConflict(g *gocui.Gui, conflict commands.Conflict, pick s
|
||||
}
|
||||
|
||||
func (gui *Gui) pushFileSnapshot(g *gocui.Gui) error {
|
||||
gitFile, err := gui.getSelectedFile(g)
|
||||
gitFile, err := gui.getSelectedFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -147,17 +156,25 @@ func (gui *Gui) handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error {
|
||||
return nil
|
||||
}
|
||||
prevContent := gui.State.Panels.Merging.EditHistory.Pop().(string)
|
||||
gitFile, err := gui.getSelectedFile(g)
|
||||
gitFile, err := gui.getSelectedFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644)
|
||||
if err := ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.refreshMergePanel()
|
||||
}
|
||||
|
||||
func (gui *Gui) handlePickHunk(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.takeOverScrolling()
|
||||
|
||||
conflict := gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex]
|
||||
gui.pushFileSnapshot(g)
|
||||
if err := gui.pushFileSnapshot(g); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pick := "bottom"
|
||||
if gui.State.Panels.Merging.ConflictTop {
|
||||
pick = "top"
|
||||
@@ -177,8 +194,12 @@ func (gui *Gui) handlePickHunk(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handlePickBothHunks(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.takeOverScrolling()
|
||||
|
||||
conflict := gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex]
|
||||
gui.pushFileSnapshot(g)
|
||||
if err := gui.pushFileSnapshot(g); err != nil {
|
||||
return err
|
||||
}
|
||||
err := gui.resolveConflict(g, conflict, "both")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -212,9 +233,7 @@ func (gui *Gui) refreshMergePanel() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.renderString(gui.g, "main", content); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.scrollToConflict(gui.g); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -222,10 +241,37 @@ func (gui *Gui) refreshMergePanel() error {
|
||||
mainView := gui.getMainView()
|
||||
mainView.Wrap = false
|
||||
|
||||
if err := gui.newStringTaskWithoutScroll("main", content); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) {
|
||||
item, err := gui.getSelectedFile()
|
||||
if err != nil {
|
||||
if err != gui.Errors.ErrNoFiles {
|
||||
return "", err
|
||||
}
|
||||
return "", gui.newStringTask("main", gui.Tr.SLocalize("NoFilesDisplay"))
|
||||
}
|
||||
if item.Type != "file" {
|
||||
return "", gui.newStringTask("main", gui.Tr.SLocalize("NotAFile"))
|
||||
}
|
||||
cat, err := gui.GitCommand.CatFile(item.Name)
|
||||
if err != nil {
|
||||
gui.Log.Error(err)
|
||||
return "", gui.newStringTask("main", err.Error())
|
||||
}
|
||||
return cat, nil
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollToConflict(g *gocui.Gui) error {
|
||||
if gui.State.Panels.Merging.UserScrolling {
|
||||
return nil
|
||||
}
|
||||
|
||||
panelState := gui.State.Panels.Merging
|
||||
if len(panelState.Conflicts) == 0 {
|
||||
return nil
|
||||
@@ -248,13 +294,15 @@ func (gui *Gui) renderMergeOptions() error {
|
||||
fmt.Sprintf("%s %s", gui.getKeyDisplay("universal.prevBlock"), gui.getKeyDisplay("universal.nextBlock")): gui.Tr.SLocalize("navigateConflicts"),
|
||||
gui.getKeyDisplay("universal.select"): gui.Tr.SLocalize("pickHunk"),
|
||||
gui.getKeyDisplay("main.pickBothHunks"): gui.Tr.SLocalize("pickBothHunks"),
|
||||
gui.getKeyDisplay("main.undo"): gui.Tr.SLocalize("undo"),
|
||||
gui.getKeyDisplay("universal.undo"): gui.Tr.SLocalize("undo"),
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleEscapeMerge(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.takeOverScrolling()
|
||||
|
||||
gui.State.Panels.Merging.EditHistory = stack.New()
|
||||
if err := gui.refreshFiles(); err != nil {
|
||||
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
|
||||
return err
|
||||
}
|
||||
// it's possible this method won't be called from the merging view so we need to
|
||||
@@ -269,12 +317,12 @@ func (gui *Gui) handleCompleteMerge() error {
|
||||
if err := gui.stageSelectedFile(gui.g); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.refreshFiles(); err != nil {
|
||||
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
|
||||
return err
|
||||
}
|
||||
// if we got conflicts after unstashing, we don't want to call any git
|
||||
// commands to continue rebasing/merging here
|
||||
if gui.State.WorkingTreeState == "normal" {
|
||||
if gui.GitCommand.WorkingTreeState() == "normal" {
|
||||
return gui.handleEscapeMerge(gui.g, gui.getMainView())
|
||||
}
|
||||
// if there are no more files with merge conflicts, we should ask whether the user wants to continue
|
||||
@@ -286,7 +334,23 @@ func (gui *Gui) handleCompleteMerge() error {
|
||||
|
||||
// promptToContinue asks the user if they want to continue the rebase/merge that's in progress
|
||||
func (gui *Gui) promptToContinue() error {
|
||||
gui.takeOverScrolling()
|
||||
|
||||
return gui.createConfirmationPanel(gui.g, gui.getFilesView(), true, "continue", gui.Tr.SLocalize("ConflictsResolved"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.genericMergeCommand("continue")
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) canScrollMergePanel() bool {
|
||||
currentViewName := gui.currentViewName()
|
||||
if currentViewName != "main" {
|
||||
return false
|
||||
}
|
||||
|
||||
file, err := gui.getSelectedFile()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return file.HasInlineMergeConflicts
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ package gui
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
@@ -38,19 +36,23 @@ func (gui *Gui) getBindings(v *gocui.View) []*Binding {
|
||||
func (gui *Gui) handleCreateOptionsMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
bindings := gui.getBindings(v)
|
||||
|
||||
handleMenuPress := func(index int) error {
|
||||
if bindings[index].Key == nil {
|
||||
return nil
|
||||
menuItems := make([]*menuItem, len(bindings))
|
||||
|
||||
for i, binding := range bindings {
|
||||
innerBinding := binding // note to self, never close over loop variables
|
||||
menuItems[i] = &menuItem{
|
||||
displayStrings: []string{GetKeyDisplay(innerBinding.Key), innerBinding.Description},
|
||||
onPress: func() error {
|
||||
if innerBinding.Key == nil {
|
||||
return nil
|
||||
}
|
||||
if err := gui.handleMenuClose(g, v); err != nil {
|
||||
return err
|
||||
}
|
||||
return innerBinding.Handler(g, v)
|
||||
},
|
||||
}
|
||||
if index >= len(bindings) {
|
||||
return errors.New("Index is greater than size of bindings")
|
||||
}
|
||||
err := gui.handleMenuClose(g, v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return bindings[index].Handler(g, v)
|
||||
}
|
||||
|
||||
return gui.createMenu(strings.Title(gui.Tr.SLocalize("menu")), bindings, len(bindings), handleMenuPress)
|
||||
return gui.createMenu(strings.Title(gui.Tr.SLocalize("menu")), menuItems, createMenuOptions{})
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package gui
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int) error {
|
||||
@@ -15,9 +16,10 @@ func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int) error {
|
||||
gui.getSecondaryView().Title = "Custom Patch"
|
||||
|
||||
// get diff from commit file that's currently selected
|
||||
commitFile := gui.getSelectedCommitFile(gui.g)
|
||||
commitFile := gui.getSelectedCommitFile()
|
||||
if commitFile == nil {
|
||||
return gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
|
||||
gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
|
||||
return nil
|
||||
}
|
||||
|
||||
diff, err := gui.GitCommand.ShowCommitFile(commitFile.Sha, commitFile.Name, true)
|
||||
@@ -42,38 +44,25 @@ func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleAddSelectionToPatch(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleToggleSelectionForPatch(g *gocui.Gui, v *gocui.View) error {
|
||||
state := gui.State.Panels.LineByLine
|
||||
|
||||
// add range of lines to those set for the file
|
||||
commitFile := gui.getSelectedCommitFile(gui.g)
|
||||
if commitFile == nil {
|
||||
return gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
|
||||
toggleFunc := gui.GitCommand.PatchManager.AddFileLineRange
|
||||
filename := gui.getSelectedCommitFileName()
|
||||
includedLineIndices := gui.GitCommand.PatchManager.GetFileIncLineIndices(filename)
|
||||
currentLineIsStaged := utils.IncludesInt(includedLineIndices, state.SelectedLineIdx)
|
||||
if currentLineIsStaged {
|
||||
toggleFunc = gui.GitCommand.PatchManager.RemoveFileLineRange
|
||||
}
|
||||
|
||||
gui.GitCommand.PatchManager.AddFileLineRange(commitFile.Name, state.FirstLineIdx, state.LastLineIdx)
|
||||
|
||||
if err := gui.refreshCommitFilesView(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.refreshPatchBuildingPanel(-1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleRemoveSelectionFromPatch(g *gocui.Gui, v *gocui.View) error {
|
||||
state := gui.State.Panels.LineByLine
|
||||
|
||||
// add range of lines to those set for the file
|
||||
commitFile := gui.getSelectedCommitFile(gui.g)
|
||||
commitFile := gui.getSelectedCommitFile()
|
||||
if commitFile == nil {
|
||||
return gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
|
||||
gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.GitCommand.PatchManager.RemoveFileLineRange(commitFile.Name, state.FirstLineIdx, state.LastLineIdx)
|
||||
toggleFunc(commitFile.Name, state.FirstLineIdx, state.LastLineIdx)
|
||||
|
||||
if err := gui.refreshCommitFilesView(); err != nil {
|
||||
return err
|
||||
@@ -87,8 +76,7 @@ func (gui *Gui) handleRemoveSelectionFromPatch(g *gocui.Gui, v *gocui.View) erro
|
||||
}
|
||||
|
||||
func (gui *Gui) handleEscapePatchBuildingPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.State.Panels.LineByLine = nil
|
||||
gui.changeMainViewsContext("normal")
|
||||
gui.handleEscapeLineByLinePanel()
|
||||
|
||||
if gui.GitCommand.PatchManager.IsEmpty() {
|
||||
gui.GitCommand.PatchManager.Reset()
|
||||
@@ -106,7 +94,8 @@ func (gui *Gui) refreshSecondaryPatchPanel() error {
|
||||
secondaryView.Wrap = false
|
||||
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
return gui.setViewContent(gui.g, gui.getSecondaryView(), gui.GitCommand.PatchManager.RenderAggregatedPatchColored(false))
|
||||
gui.setViewContent(gui.getSecondaryView(), gui.GitCommand.PatchManager.RenderAggregatedPatchColored(false))
|
||||
return nil
|
||||
})
|
||||
} else {
|
||||
gui.State.SplitMainPanel = false
|
||||
|
||||
@@ -6,48 +6,55 @@ import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
type patchMenuOption struct {
|
||||
displayName string
|
||||
function func() error
|
||||
}
|
||||
|
||||
// GetDisplayStrings is a function.
|
||||
func (o *patchMenuOption) GetDisplayStrings(isFocused bool) []string {
|
||||
return []string{o.displayName}
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreatePatchOptionsMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
if !gui.GitCommand.PatchManager.CommitSelected() {
|
||||
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NoPatchError"))
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("NoPatchError"))
|
||||
}
|
||||
|
||||
options := []*patchMenuOption{
|
||||
{displayName: fmt.Sprintf("remove patch from original commit (%s)", gui.GitCommand.PatchManager.CommitSha), function: gui.handleDeletePatchFromCommit},
|
||||
{displayName: "pull patch out into index", function: gui.handlePullPatchIntoWorkingTree},
|
||||
{displayName: "reset patch", function: gui.handleResetPatch},
|
||||
menuItems := []*menuItem{
|
||||
{
|
||||
displayString: fmt.Sprintf("remove patch from original commit (%s)", gui.GitCommand.PatchManager.CommitSha),
|
||||
onPress: gui.handleDeletePatchFromCommit,
|
||||
},
|
||||
{
|
||||
displayString: "pull patch out into index",
|
||||
onPress: gui.handlePullPatchIntoWorkingTree,
|
||||
},
|
||||
{
|
||||
displayString: "pull patch into new commit",
|
||||
onPress: gui.handlePullPatchIntoNewCommit,
|
||||
},
|
||||
{
|
||||
displayString: "apply patch",
|
||||
onPress: func() error { return gui.handleApplyPatch(false) },
|
||||
},
|
||||
{
|
||||
displayString: "apply patch in reverse",
|
||||
onPress: func() error { return gui.handleApplyPatch(true) },
|
||||
},
|
||||
{
|
||||
displayString: "reset patch",
|
||||
onPress: gui.handleResetPatch,
|
||||
},
|
||||
}
|
||||
|
||||
selectedCommit := gui.getSelectedCommit(gui.g)
|
||||
selectedCommit := gui.getSelectedCommit()
|
||||
if selectedCommit != nil && gui.GitCommand.PatchManager.CommitSha != selectedCommit.Sha {
|
||||
// adding this option to index 1
|
||||
options = append(
|
||||
options[:1],
|
||||
menuItems = append(
|
||||
menuItems[:1],
|
||||
append(
|
||||
[]*patchMenuOption{
|
||||
[]*menuItem{
|
||||
{
|
||||
displayName: fmt.Sprintf("move patch to selected commit (%s)", selectedCommit.Sha),
|
||||
function: gui.handleMovePatchToSelectedCommit,
|
||||
displayString: fmt.Sprintf("move patch to selected commit (%s)", selectedCommit.Sha),
|
||||
onPress: gui.handleMovePatchToSelectedCommit,
|
||||
},
|
||||
}, options[1:]...,
|
||||
}, menuItems[1:]...,
|
||||
)...,
|
||||
)
|
||||
}
|
||||
|
||||
handleMenuPress := func(index int) error {
|
||||
return options[index].function()
|
||||
}
|
||||
|
||||
return gui.createMenu(gui.Tr.SLocalize("PatchOptionsTitle"), options, len(options), handleMenuPress)
|
||||
return gui.createMenu(gui.Tr.SLocalize("PatchOptionsTitle"), menuItems, createMenuOptions{showCancel: true})
|
||||
}
|
||||
|
||||
func (gui *Gui) getPatchCommitIndex() int {
|
||||
@@ -60,8 +67,11 @@ func (gui *Gui) getPatchCommitIndex() int {
|
||||
}
|
||||
|
||||
func (gui *Gui) validateNormalWorkingTreeState() (bool, error) {
|
||||
if gui.State.WorkingTreeState != "normal" {
|
||||
return false, gui.createErrorPanel(gui.g, gui.Tr.SLocalize("CantPatchWhileRebasingError"))
|
||||
if gui.GitCommand.WorkingTreeState() != "normal" {
|
||||
return false, gui.createErrorPanel(gui.Tr.SLocalize("CantPatchWhileRebasingError"))
|
||||
}
|
||||
if gui.GitCommand.WorkingTreeState() != "normal" {
|
||||
return false, gui.createErrorPanel(gui.Tr.SLocalize("CantPatchWhileRebasingError"))
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
@@ -114,13 +124,50 @@ func (gui *Gui) handlePullPatchIntoWorkingTree() error {
|
||||
return err
|
||||
}
|
||||
|
||||
pull := func(stash bool) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
|
||||
commitIndex := gui.getPatchCommitIndex()
|
||||
err := gui.GitCommand.PullPatchIntoIndex(gui.State.Commits, commitIndex, gui.GitCommand.PatchManager, stash)
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
}
|
||||
|
||||
if len(gui.trackedFiles()) > 0 {
|
||||
return gui.createConfirmationPanel(gui.g, gui.g.CurrentView(), true, gui.Tr.SLocalize("MustStashTitle"), gui.Tr.SLocalize("MustStashWarning"), func(*gocui.Gui, *gocui.View) error {
|
||||
return pull(true)
|
||||
}, nil)
|
||||
} else {
|
||||
return pull(false)
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) handlePullPatchIntoNewCommit() error {
|
||||
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.returnFocusFromLineByLinePanelIfNecessary(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
|
||||
commitIndex := gui.getPatchCommitIndex()
|
||||
err := gui.GitCommand.PullPatchIntoIndex(gui.State.Commits, commitIndex, gui.GitCommand.PatchManager)
|
||||
err := gui.GitCommand.PullPatchIntoNewCommit(gui.State.Commits, commitIndex, gui.GitCommand.PatchManager)
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleApplyPatch(reverse bool) error {
|
||||
if err := gui.returnFocusFromLineByLinePanelIfNecessary(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.GitCommand.PatchManager.ApplyPatches(reverse); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleResetPatch() error {
|
||||
gui.GitCommand.PatchManager.Reset()
|
||||
return gui.refreshCommitFilesView()
|
||||
|
||||
71
pkg/gui/presentation/branches.go
Normal file
71
pkg/gui/presentation/branches.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package presentation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func GetBranchListDisplayStrings(branches []*commands.Branch, fullDescription bool, diffName string) [][]string {
|
||||
lines := make([][]string, len(branches))
|
||||
|
||||
for i := range branches {
|
||||
diffed := branches[i].Name == diffName
|
||||
lines[i] = getBranchDisplayStrings(branches[i], fullDescription, diffed)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
// getBranchDisplayStrings returns the display string of branch
|
||||
func getBranchDisplayStrings(b *commands.Branch, fullDescription bool, diffed bool) []string {
|
||||
displayName := b.Name
|
||||
if b.DisplayName != "" {
|
||||
displayName = b.DisplayName
|
||||
}
|
||||
|
||||
nameColorAttr := GetBranchColor(b.Name)
|
||||
if diffed {
|
||||
nameColorAttr = theme.DiffTerminalColor
|
||||
}
|
||||
coloredName := utils.ColoredString(displayName, nameColorAttr)
|
||||
if b.Pushables != "" && b.Pullables != "" && b.Pushables != "?" && b.Pullables != "?" {
|
||||
trackColor := color.FgYellow
|
||||
if b.Pushables == "0" && b.Pullables == "0" {
|
||||
trackColor = color.FgGreen
|
||||
}
|
||||
track := utils.ColoredString(fmt.Sprintf("↑%s↓%s", b.Pushables, b.Pullables), trackColor)
|
||||
coloredName = fmt.Sprintf("%s %s", coloredName, track)
|
||||
}
|
||||
|
||||
recencyColor := color.FgCyan
|
||||
if b.Recency == " *" {
|
||||
recencyColor = color.FgGreen
|
||||
}
|
||||
|
||||
if fullDescription {
|
||||
return []string{utils.ColoredString(b.Recency, recencyColor), coloredName, utils.ColoredString(b.UpstreamName, color.FgYellow)}
|
||||
}
|
||||
|
||||
return []string{utils.ColoredString(b.Recency, recencyColor), coloredName}
|
||||
}
|
||||
|
||||
// GetBranchColor branch color
|
||||
func GetBranchColor(name string) color.Attribute {
|
||||
branchType := strings.Split(name, "/")[0]
|
||||
|
||||
switch branchType {
|
||||
case "feature":
|
||||
return color.FgGreen
|
||||
case "bugfix":
|
||||
return color.FgYellow
|
||||
case "hotfix":
|
||||
return color.FgRed
|
||||
default:
|
||||
return theme.DefaultTextColor
|
||||
}
|
||||
}
|
||||
40
pkg/gui/presentation/commit_files.go
Normal file
40
pkg/gui/presentation/commit_files.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package presentation
|
||||
|
||||
import (
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
)
|
||||
|
||||
func GetCommitFileListDisplayStrings(commitFiles []*commands.CommitFile, diffName string) [][]string {
|
||||
lines := make([][]string, len(commitFiles))
|
||||
|
||||
for i := range commitFiles {
|
||||
diffed := commitFiles[i].Name == diffName
|
||||
lines[i] = getCommitFileDisplayStrings(commitFiles[i], diffed)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
// getCommitFileDisplayStrings returns the display string of branch
|
||||
func getCommitFileDisplayStrings(f *commands.CommitFile, diffed bool) []string {
|
||||
yellow := color.New(color.FgYellow)
|
||||
green := color.New(color.FgGreen)
|
||||
defaultColor := color.New(theme.DefaultTextColor)
|
||||
diffTerminalColor := color.New(theme.DiffTerminalColor)
|
||||
|
||||
var colour *color.Color
|
||||
switch f.Status {
|
||||
case commands.UNSELECTED:
|
||||
colour = defaultColor
|
||||
case commands.WHOLE:
|
||||
colour = green
|
||||
case commands.PART:
|
||||
colour = yellow
|
||||
}
|
||||
if diffed {
|
||||
colour = diffTerminalColor
|
||||
}
|
||||
return []string{colour.Sprint(f.DisplayString)}
|
||||
}
|
||||
126
pkg/gui/presentation/commits.go
Normal file
126
pkg/gui/presentation/commits.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package presentation
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func GetCommitListDisplayStrings(commits []*commands.Commit, fullDescription bool, cherryPickedCommitShaMap map[string]bool, diffName string) [][]string {
|
||||
lines := make([][]string, len(commits))
|
||||
|
||||
var displayFunc func(*commands.Commit, map[string]bool, bool) []string
|
||||
if fullDescription {
|
||||
displayFunc = getFullDescriptionDisplayStringsForCommit
|
||||
} else {
|
||||
displayFunc = getDisplayStringsForCommit
|
||||
}
|
||||
|
||||
for i := range commits {
|
||||
diffed := commits[i].Sha == diffName
|
||||
lines[i] = displayFunc(commits[i], cherryPickedCommitShaMap, diffed)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
func getFullDescriptionDisplayStringsForCommit(c *commands.Commit, cherryPickedCommitShaMap map[string]bool, diffed bool) []string {
|
||||
red := color.New(color.FgRed)
|
||||
yellow := color.New(color.FgYellow)
|
||||
green := color.New(color.FgGreen)
|
||||
blue := color.New(color.FgBlue)
|
||||
cyan := color.New(color.FgCyan)
|
||||
defaultColor := color.New(theme.DefaultTextColor)
|
||||
diffedColor := color.New(theme.DiffTerminalColor)
|
||||
|
||||
// for some reason, setting the background to blue pads out the other commits
|
||||
// horizontally. For the sake of accessibility I'm considering this a feature,
|
||||
// not a bug
|
||||
copied := color.New(color.FgCyan, color.BgBlue)
|
||||
|
||||
var shaColor *color.Color
|
||||
switch c.Status {
|
||||
case "unpushed":
|
||||
shaColor = red
|
||||
case "pushed":
|
||||
shaColor = yellow
|
||||
case "merged":
|
||||
shaColor = green
|
||||
case "rebasing":
|
||||
shaColor = blue
|
||||
case "reflog":
|
||||
shaColor = blue
|
||||
default:
|
||||
shaColor = defaultColor
|
||||
}
|
||||
|
||||
if diffed {
|
||||
shaColor = diffedColor
|
||||
} else if cherryPickedCommitShaMap[c.Sha] {
|
||||
shaColor = copied
|
||||
}
|
||||
|
||||
tagString := ""
|
||||
secondColumnString := blue.Sprint(utils.UnixToDate(c.UnixTimestamp))
|
||||
if c.Action != "" {
|
||||
secondColumnString = cyan.Sprint(c.Action)
|
||||
} else if c.ExtraInfo != "" {
|
||||
tagColor := color.New(color.FgMagenta, color.Bold)
|
||||
tagString = utils.ColoredStringDirect(c.ExtraInfo, tagColor) + " "
|
||||
}
|
||||
|
||||
truncatedAuthor := utils.TruncateWithEllipsis(c.Author, 17)
|
||||
|
||||
return []string{shaColor.Sprint(c.ShortSha()), secondColumnString, yellow.Sprint(truncatedAuthor), tagString + defaultColor.Sprint(c.Name)}
|
||||
}
|
||||
|
||||
func getDisplayStringsForCommit(c *commands.Commit, cherryPickedCommitShaMap map[string]bool, diffed bool) []string {
|
||||
red := color.New(color.FgRed)
|
||||
yellow := color.New(color.FgYellow)
|
||||
green := color.New(color.FgGreen)
|
||||
blue := color.New(color.FgBlue)
|
||||
cyan := color.New(color.FgCyan)
|
||||
defaultColor := color.New(theme.DefaultTextColor)
|
||||
diffedColor := color.New(theme.DiffTerminalColor)
|
||||
|
||||
// for some reason, setting the background to blue pads out the other commits
|
||||
// horizontally. For the sake of accessibility I'm considering this a feature,
|
||||
// not a bug
|
||||
copied := color.New(color.FgCyan, color.BgBlue)
|
||||
|
||||
var shaColor *color.Color
|
||||
switch c.Status {
|
||||
case "unpushed":
|
||||
shaColor = red
|
||||
case "pushed":
|
||||
shaColor = yellow
|
||||
case "merged":
|
||||
shaColor = green
|
||||
case "rebasing":
|
||||
shaColor = blue
|
||||
case "reflog":
|
||||
shaColor = blue
|
||||
default:
|
||||
shaColor = defaultColor
|
||||
}
|
||||
|
||||
if diffed {
|
||||
shaColor = diffedColor
|
||||
} else if cherryPickedCommitShaMap[c.Sha] {
|
||||
shaColor = copied
|
||||
}
|
||||
|
||||
actionString := ""
|
||||
tagString := ""
|
||||
if c.Action != "" {
|
||||
actionString = cyan.Sprint(utils.WithPadding(c.Action, 7)) + " "
|
||||
} else if len(c.Tags) > 0 {
|
||||
tagColor := color.New(color.FgMagenta, color.Bold)
|
||||
tagString = utils.ColoredStringDirect(strings.Join(c.Tags, " "), tagColor) + " "
|
||||
}
|
||||
|
||||
return []string{shaColor.Sprint(c.ShortSha()), actionString + tagString + defaultColor.Sprint(c.Name)}
|
||||
}
|
||||
57
pkg/gui/presentation/files.go
Normal file
57
pkg/gui/presentation/files.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package presentation
|
||||
|
||||
import (
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
)
|
||||
|
||||
func GetFileListDisplayStrings(files []*commands.File, diffName string) [][]string {
|
||||
lines := make([][]string, len(files))
|
||||
|
||||
for i := range files {
|
||||
diffed := files[i].Name == diffName
|
||||
lines[i] = getFileDisplayStrings(files[i], diffed)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
// getFileDisplayStrings returns the display string of branch
|
||||
func getFileDisplayStrings(f *commands.File, diffed bool) []string {
|
||||
// potentially inefficient to be instantiating these color
|
||||
// objects with each render
|
||||
red := color.New(color.FgRed)
|
||||
green := color.New(color.FgGreen)
|
||||
diffColor := color.New(theme.DiffTerminalColor)
|
||||
if !f.Tracked && !f.HasStagedChanges {
|
||||
return []string{red.Sprint(f.DisplayString)}
|
||||
}
|
||||
|
||||
var restColor *color.Color
|
||||
if diffed {
|
||||
restColor = diffColor
|
||||
} else if f.HasUnstagedChanges {
|
||||
restColor = red
|
||||
} else {
|
||||
restColor = green
|
||||
}
|
||||
|
||||
// this is just making things look nice when the background attribute is 'reverse'
|
||||
firstChar := f.DisplayString[0:1]
|
||||
firstCharCl := green
|
||||
if firstChar == " " {
|
||||
firstCharCl = restColor
|
||||
}
|
||||
|
||||
secondChar := f.DisplayString[1:2]
|
||||
secondCharCl := red
|
||||
if secondChar == " " {
|
||||
secondCharCl = restColor
|
||||
}
|
||||
|
||||
output := firstCharCl.Sprint(firstChar)
|
||||
output += secondCharCl.Sprint(secondChar)
|
||||
output += restColor.Sprintf(" %s", f.Name)
|
||||
return []string{output}
|
||||
}
|
||||
45
pkg/gui/presentation/reflog_commits.go
Normal file
45
pkg/gui/presentation/reflog_commits.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package presentation
|
||||
|
||||
import (
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func GetReflogCommitListDisplayStrings(commits []*commands.Commit, fullDescription bool, diffName string) [][]string {
|
||||
lines := make([][]string, len(commits))
|
||||
|
||||
var displayFunc func(*commands.Commit, bool) []string
|
||||
if fullDescription {
|
||||
displayFunc = getFullDescriptionDisplayStringsForReflogCommit
|
||||
} else {
|
||||
displayFunc = getDisplayStringsForReflogCommit
|
||||
}
|
||||
|
||||
for i := range commits {
|
||||
diffed := commits[i].Sha == diffName
|
||||
lines[i] = displayFunc(commits[i], diffed)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
func getFullDescriptionDisplayStringsForReflogCommit(c *commands.Commit, diffed bool) []string {
|
||||
colorAttr := theme.DefaultTextColor
|
||||
if diffed {
|
||||
colorAttr = theme.DiffTerminalColor
|
||||
}
|
||||
|
||||
return []string{
|
||||
utils.ColoredString(c.ShortSha(), color.FgBlue),
|
||||
utils.ColoredString(utils.UnixToDate(c.UnixTimestamp), color.FgMagenta),
|
||||
utils.ColoredString(c.Name, colorAttr),
|
||||
}
|
||||
}
|
||||
|
||||
func getDisplayStringsForReflogCommit(c *commands.Commit, diffed bool) []string {
|
||||
defaultColor := color.New(theme.DefaultTextColor)
|
||||
|
||||
return []string{utils.ColoredString(c.ShortSha(), color.FgBlue), defaultColor.Sprint(c.Name)}
|
||||
}
|
||||
30
pkg/gui/presentation/remote_branches.go
Normal file
30
pkg/gui/presentation/remote_branches.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package presentation
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func GetRemoteBranchListDisplayStrings(branches []*commands.RemoteBranch, diffName string) [][]string {
|
||||
lines := make([][]string, len(branches))
|
||||
|
||||
for i := range branches {
|
||||
diffed := branches[i].FullName() == diffName
|
||||
lines[i] = getRemoteBranchDisplayStrings(branches[i], diffed)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
// getRemoteBranchDisplayStrings returns the display string of branch
|
||||
func getRemoteBranchDisplayStrings(b *commands.RemoteBranch, diffed bool) []string {
|
||||
nameColorAttr := GetBranchColor(b.Name)
|
||||
if diffed {
|
||||
nameColorAttr = theme.DiffTerminalColor
|
||||
}
|
||||
|
||||
displayName := utils.ColoredString(b.Name, nameColorAttr)
|
||||
|
||||
return []string{displayName}
|
||||
}
|
||||
33
pkg/gui/presentation/remotes.go
Normal file
33
pkg/gui/presentation/remotes.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package presentation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func GetRemoteListDisplayStrings(remotes []*commands.Remote, diffName string) [][]string {
|
||||
lines := make([][]string, len(remotes))
|
||||
|
||||
for i := range remotes {
|
||||
diffed := remotes[i].Name == diffName
|
||||
lines[i] = getRemoteDisplayStrings(remotes[i], diffed)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
// getRemoteDisplayStrings returns the display string of branch
|
||||
func getRemoteDisplayStrings(r *commands.Remote, diffed bool) []string {
|
||||
branchCount := len(r.Branches)
|
||||
|
||||
nameColorAttr := theme.DefaultTextColor
|
||||
if diffed {
|
||||
nameColorAttr = theme.DiffTerminalColor
|
||||
}
|
||||
|
||||
return []string{utils.ColoredString(r.Name, nameColorAttr), utils.ColoredString(fmt.Sprintf("%d branches", branchCount), color.FgBlue)}
|
||||
}
|
||||
27
pkg/gui/presentation/stash_entries.go
Normal file
27
pkg/gui/presentation/stash_entries.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package presentation
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func GetStashEntryListDisplayStrings(stashEntries []*commands.StashEntry, diffName string) [][]string {
|
||||
lines := make([][]string, len(stashEntries))
|
||||
|
||||
for i := range stashEntries {
|
||||
diffed := stashEntries[i].RefName() == diffName
|
||||
lines[i] = getStashEntryDisplayStrings(stashEntries[i], diffed)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
// getStashEntryDisplayStrings returns the display string of branch
|
||||
func getStashEntryDisplayStrings(s *commands.StashEntry, diffed bool) []string {
|
||||
attr := theme.DefaultTextColor
|
||||
if diffed {
|
||||
attr = theme.DiffTerminalColor
|
||||
}
|
||||
return []string{utils.ColoredString(s.Name, attr)}
|
||||
}
|
||||
27
pkg/gui/presentation/tags.go
Normal file
27
pkg/gui/presentation/tags.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package presentation
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func GetTagListDisplayStrings(tags []*commands.Tag, diffName string) [][]string {
|
||||
lines := make([][]string, len(tags))
|
||||
|
||||
for i := range tags {
|
||||
diffed := tags[i].Name == diffName
|
||||
lines[i] = getTagDisplayStrings(tags[i], diffed)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
// getTagDisplayStrings returns the display string of branch
|
||||
func getTagDisplayStrings(t *commands.Tag, diffed bool) []string {
|
||||
attr := theme.DefaultTextColor
|
||||
if diffed {
|
||||
attr = theme.DiffTerminalColor
|
||||
}
|
||||
return []string{utils.ColoredString(t.Name, attr)}
|
||||
}
|
||||
74
pkg/gui/pty.go
Normal file
74
pkg/gui/pty.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// +build !windows
|
||||
|
||||
package gui
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
|
||||
"github.com/creack/pty"
|
||||
)
|
||||
|
||||
func (gui *Gui) onResize() error {
|
||||
if gui.State.Ptmx == nil {
|
||||
return nil
|
||||
}
|
||||
mainView := gui.getMainView()
|
||||
width, height := mainView.Size()
|
||||
|
||||
if err := pty.Setsize(gui.State.Ptmx, &pty.Winsize{Cols: uint16(width), Rows: uint16(height)}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: handle resizing properly
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Some commands need to output for a terminal to active certain behaviour.
|
||||
// For example, git won't invoke the GIT_PAGER env var unless it thinks it's
|
||||
// talking to a terminal. We typically write cmd outputs straight to a view,
|
||||
// which is just an io.Reader. the pty package lets us wrap a command in a
|
||||
// pseudo-terminal meaning we'll get the behaviour we want from the underlying
|
||||
// command.
|
||||
func (gui *Gui) newPtyTask(viewName string, cmd *exec.Cmd) error {
|
||||
width, _ := gui.getMainView().Size()
|
||||
pager := gui.GitCommand.GetPager(width)
|
||||
|
||||
if pager == "" {
|
||||
// if we're not using a custom pager we don't need to use a pty
|
||||
return gui.newCmdTask(viewName, cmd)
|
||||
}
|
||||
|
||||
cmd.Env = append(cmd.Env, "GIT_PAGER="+pager)
|
||||
|
||||
view, err := gui.g.View(viewName)
|
||||
if err != nil {
|
||||
return nil // swallowing for now
|
||||
}
|
||||
|
||||
_, height := view.Size()
|
||||
_, oy := view.Origin()
|
||||
|
||||
manager := gui.getManager(view)
|
||||
|
||||
ptmx, err := pty.Start(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.State.Ptmx = ptmx
|
||||
onClose := func() {
|
||||
ptmx.Close()
|
||||
gui.State.Ptmx = nil
|
||||
}
|
||||
|
||||
if err := gui.onResize(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := manager.NewTask(manager.NewCmdTask(ptmx, cmd, height+oy+10, onClose)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
13
pkg/gui/pty_windows.go
Normal file
13
pkg/gui/pty_windows.go
Normal file
@@ -0,0 +1,13 @@
|
||||
// +build windows
|
||||
|
||||
package gui
|
||||
|
||||
import "os/exec"
|
||||
|
||||
func (gui *Gui) onResize() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) newPtyTask(viewName string, cmd *exec.Cmd) error {
|
||||
return gui.newCmdTask(viewName, cmd)
|
||||
}
|
||||
@@ -34,10 +34,24 @@ func (gui *Gui) handleQuit(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.quit(v)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleTopLevelReturn(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.Config.GetUserConfig().GetBool("quitOnTopLevelReturn") {
|
||||
return gui.handleQuit(g, v)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) quit(v *gocui.View) error {
|
||||
if gui.State.Updating {
|
||||
return gui.createUpdateQuitConfirmation(gui.g, v)
|
||||
}
|
||||
if gui.inDiffMode() {
|
||||
return gui.exitDiffMode()
|
||||
}
|
||||
if gui.inFilterMode() {
|
||||
return gui.exitFilterMode()
|
||||
}
|
||||
if gui.Config.GetUserConfig().GetBool("confirmOnQuit") {
|
||||
return gui.createConfirmationPanel(gui.g, v, true, "", gui.Tr.SLocalize("ConfirmQuit"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gocui.ErrQuit
|
||||
|
||||
@@ -7,50 +7,40 @@ import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
type option struct {
|
||||
value string
|
||||
}
|
||||
|
||||
// GetDisplayStrings is a function.
|
||||
func (r *option) GetDisplayStrings(isFocused bool) []string {
|
||||
return []string{r.value}
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateRebaseOptionsMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
options := []*option{
|
||||
{value: "continue"},
|
||||
{value: "abort"},
|
||||
options := []string{"continue", "abort"}
|
||||
|
||||
if gui.GitCommand.WorkingTreeState() == "rebasing" {
|
||||
options = append(options, "skip")
|
||||
}
|
||||
|
||||
if gui.State.WorkingTreeState == "rebasing" {
|
||||
options = append(options, &option{value: "skip"})
|
||||
}
|
||||
|
||||
options = append(options, &option{value: "cancel"})
|
||||
|
||||
handleMenuPress := func(index int) error {
|
||||
command := options[index].value
|
||||
if command == "cancel" {
|
||||
return nil
|
||||
menuItems := make([]*menuItem, len(options))
|
||||
for i, option := range options {
|
||||
// note to self. Never, EVER, close over loop variables in a function
|
||||
option := option
|
||||
menuItems[i] = &menuItem{
|
||||
displayString: option,
|
||||
onPress: func() error {
|
||||
return gui.genericMergeCommand(option)
|
||||
},
|
||||
}
|
||||
return gui.genericMergeCommand(command)
|
||||
}
|
||||
|
||||
var title string
|
||||
if gui.State.WorkingTreeState == "merging" {
|
||||
if gui.GitCommand.WorkingTreeState() == "merging" {
|
||||
title = gui.Tr.SLocalize("MergeOptionsTitle")
|
||||
} else {
|
||||
title = gui.Tr.SLocalize("RebaseOptionsTitle")
|
||||
}
|
||||
|
||||
return gui.createMenu(title, options, len(options), handleMenuPress)
|
||||
return gui.createMenu(title, menuItems, createMenuOptions{showCancel: true})
|
||||
}
|
||||
|
||||
func (gui *Gui) genericMergeCommand(command string) error {
|
||||
status := gui.State.WorkingTreeState
|
||||
status := gui.GitCommand.WorkingTreeState()
|
||||
|
||||
if status != "merging" && status != "rebasing" {
|
||||
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NotMergingOrRebasing"))
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("NotMergingOrRebasing"))
|
||||
}
|
||||
|
||||
commandType := strings.Replace(status, "ing", "e", 1)
|
||||
@@ -73,7 +63,7 @@ func (gui *Gui) genericMergeCommand(command string) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleGenericMergeCommandResult(result error) error {
|
||||
if err := gui.refreshSidePanels(gui.g); err != nil {
|
||||
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil {
|
||||
return err
|
||||
}
|
||||
if result == nil {
|
||||
@@ -84,6 +74,9 @@ func (gui *Gui) handleGenericMergeCommandResult(result error) error {
|
||||
return gui.genericMergeCommand("skip")
|
||||
} else if strings.Contains(result.Error(), "The previous cherry-pick is now empty") {
|
||||
return gui.genericMergeCommand("continue")
|
||||
} else if strings.Contains(result.Error(), "No rebase in progress?") {
|
||||
// assume in this case that we're already done
|
||||
return nil
|
||||
} else if strings.Contains(result.Error(), "When you have resolved this problem") || strings.Contains(result.Error(), "fix conflicts") || strings.Contains(result.Error(), "Resolve all conflicts manually") {
|
||||
return gui.createConfirmationPanel(gui.g, gui.getFilesView(), true, gui.Tr.SLocalize("FoundConflictsTitle"), gui.Tr.SLocalize("FoundConflicts"),
|
||||
func(g *gocui.Gui, v *gocui.View) error {
|
||||
@@ -93,6 +86,6 @@ func (gui *Gui) handleGenericMergeCommandResult(result error) error {
|
||||
},
|
||||
)
|
||||
} else {
|
||||
return gui.createErrorPanel(gui.g, result.Error())
|
||||
return gui.createErrorPanel(result.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,41 +10,35 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type recentRepo struct {
|
||||
path string
|
||||
}
|
||||
|
||||
// GetDisplayStrings returns the path from a recent repo.
|
||||
func (r *recentRepo) GetDisplayStrings(isFocused bool) []string {
|
||||
yellow := color.New(color.FgMagenta)
|
||||
base := filepath.Base(r.path)
|
||||
path := yellow.Sprint(r.path)
|
||||
return []string{base, path}
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateRecentReposMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
recentRepoPaths := gui.Config.GetAppState().RecentRepos
|
||||
reposCount := utils.Min(len(recentRepoPaths), 20)
|
||||
yellow := color.New(color.FgMagenta)
|
||||
// we won't show the current repo hence the -1
|
||||
recentRepos := make([]*recentRepo, reposCount-1)
|
||||
menuItems := make([]*menuItem, reposCount-1)
|
||||
for i, path := range recentRepoPaths[1:reposCount] {
|
||||
recentRepos[i] = &recentRepo{path: path}
|
||||
innerPath := path
|
||||
menuItems[i] = &menuItem{
|
||||
displayStrings: []string{
|
||||
filepath.Base(innerPath),
|
||||
yellow.Sprint(innerPath),
|
||||
},
|
||||
onPress: func() error {
|
||||
if err := os.Chdir(innerPath); err != nil {
|
||||
return err
|
||||
}
|
||||
newGitCommand, err := commands.NewGitCommand(gui.Log, gui.OSCommand, gui.Tr, gui.Config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gui.GitCommand = newGitCommand
|
||||
gui.State.FilterPath = ""
|
||||
return gui.Errors.ErrSwitchRepo
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
handleMenuPress := func(index int) error {
|
||||
repo := recentRepos[index]
|
||||
if err := os.Chdir(repo.path); err != nil {
|
||||
return err
|
||||
}
|
||||
newGitCommand, err := commands.NewGitCommand(gui.Log, gui.OSCommand, gui.Tr, gui.Config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gui.GitCommand = newGitCommand
|
||||
return gui.Errors.ErrSwitchRepo
|
||||
}
|
||||
|
||||
return gui.createMenu(gui.Tr.SLocalize("RecentRepos"), recentRepos, len(recentRepos), handleMenuPress)
|
||||
return gui.createMenu(gui.Tr.SLocalize("RecentRepos"), menuItems, createMenuOptions{showCancel: true})
|
||||
}
|
||||
|
||||
// updateRecentRepoList registers the fact that we opened lazygit in this repo,
|
||||
|
||||
@@ -3,17 +3,19 @@ package gui
|
||||
import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
)
|
||||
|
||||
// list panel functions
|
||||
|
||||
func (gui *Gui) getSelectedReflogCommit() *commands.Commit {
|
||||
selectedLine := gui.State.Panels.ReflogCommits.SelectedLine
|
||||
if selectedLine == -1 || len(gui.State.ReflogCommits) == 0 {
|
||||
reflogComits := gui.State.FilteredReflogCommits
|
||||
if selectedLine == -1 || len(reflogComits) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.State.ReflogCommits[selectedLine]
|
||||
return reflogComits[selectedLine]
|
||||
}
|
||||
|
||||
func (gui *Gui) handleReflogCommitSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
@@ -31,26 +33,65 @@ func (gui *Gui) handleReflogCommitSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
|
||||
commit := gui.getSelectedReflogCommit()
|
||||
if commit == nil {
|
||||
return gui.renderString(g, "main", "No reflog history")
|
||||
return gui.newStringTask("main", "No reflog history")
|
||||
}
|
||||
if err := gui.focusPoint(0, gui.State.Panels.ReflogCommits.SelectedLine, len(gui.State.ReflogCommits), v); err != nil {
|
||||
return err
|
||||
v.FocusPoint(0, gui.State.Panels.ReflogCommits.SelectedLine)
|
||||
|
||||
if gui.inDiffMode() {
|
||||
return gui.renderDiff()
|
||||
}
|
||||
|
||||
commitText, err := gui.GitCommand.Show(commit.Sha)
|
||||
if err != nil {
|
||||
return err
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.FilterPath),
|
||||
)
|
||||
if err := gui.newPtyTask("main", cmd); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
return gui.renderString(g, "main", commitText)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// the reflogs panel is the only panel where we cache data, in that we only
|
||||
// load entries that have been created since we last ran the call. This means
|
||||
// we need to be more careful with how we use this, and to ensure we're emptying
|
||||
// the reflogs array when changing contexts.
|
||||
// This method also manages two things: ReflogCommits and FilteredReflogCommits.
|
||||
// FilteredReflogCommits are rendered in the reflogs panel, and ReflogCommits
|
||||
// are used by the branches panel to obtain recency values for sorting.
|
||||
func (gui *Gui) refreshReflogCommits() error {
|
||||
commits, err := gui.GitCommand.GetReflogCommits()
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
// pulling state into its own variable incase it gets swapped out for another state
|
||||
// and we get an out of bounds exception
|
||||
state := gui.State
|
||||
var lastReflogCommit *commands.Commit
|
||||
if len(state.ReflogCommits) > 0 {
|
||||
lastReflogCommit = state.ReflogCommits[0]
|
||||
}
|
||||
|
||||
gui.State.ReflogCommits = commits
|
||||
refresh := func(stateCommits *[]*commands.Commit, filterPath string) error {
|
||||
commits, onlyObtainedNewReflogCommits, err := gui.GitCommand.GetReflogCommits(lastReflogCommit, filterPath)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
if onlyObtainedNewReflogCommits {
|
||||
*stateCommits = append(commits, *stateCommits...)
|
||||
} else {
|
||||
*stateCommits = commits
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := refresh(&state.ReflogCommits, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if gui.inFilterMode() {
|
||||
if err := refresh(&state.FilteredReflogCommits, state.FilterPath); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
state.FilteredReflogCommits = state.ReflogCommits
|
||||
}
|
||||
|
||||
if gui.getCommitsView().Context == "reflog-commits" {
|
||||
return gui.renderReflogCommitsWithSelection()
|
||||
@@ -62,10 +103,9 @@ func (gui *Gui) refreshReflogCommits() error {
|
||||
func (gui *Gui) renderReflogCommitsWithSelection() error {
|
||||
commitsView := gui.getCommitsView()
|
||||
|
||||
gui.refreshSelectedLine(&gui.State.Panels.ReflogCommits.SelectedLine, len(gui.State.ReflogCommits))
|
||||
if err := gui.renderListPanel(commitsView, gui.State.ReflogCommits); err != nil {
|
||||
return err
|
||||
}
|
||||
gui.refreshSelectedLine(&gui.State.Panels.ReflogCommits.SelectedLine, len(gui.State.FilteredReflogCommits))
|
||||
displayStrings := presentation.GetReflogCommitListDisplayStrings(gui.State.FilteredReflogCommits, gui.State.ScreenMode != SCREEN_NORMAL, gui.State.Diff.Ref)
|
||||
gui.renderDisplayStrings(commitsView, displayStrings)
|
||||
if gui.g.CurrentView() == commitsView && commitsView.Context == "reflog-commits" {
|
||||
if err := gui.handleReflogCommitSelect(gui.g, commitsView); err != nil {
|
||||
return err
|
||||
@@ -82,7 +122,7 @@ func (gui *Gui) handleCheckoutReflogCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
err := gui.createConfirmationPanel(g, gui.getCommitsView(), true, gui.Tr.SLocalize("checkoutCommit"), gui.Tr.SLocalize("SureCheckoutThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.handleCheckoutRef(commit.Sha)
|
||||
return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{})
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -92,3 +132,9 @@ func (gui *Gui) handleCheckoutReflogCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateReflogResetMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
commit := gui.getSelectedReflogCommit()
|
||||
|
||||
return gui.createResetMenu(commit.Sha)
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
type reflogResetOption struct {
|
||||
handler func() error
|
||||
description string
|
||||
command string
|
||||
}
|
||||
|
||||
// GetDisplayStrings is a function.
|
||||
func (r *reflogResetOption) GetDisplayStrings(isFocused bool) []string {
|
||||
return []string{r.description, color.New(color.FgRed).Sprint(r.command)}
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateReflogResetMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
commit := gui.getSelectedReflogCommit()
|
||||
|
||||
resetFunction := func(reset func(string) error) func() error {
|
||||
return func() error {
|
||||
if err := reset(commit.Sha); err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
}
|
||||
|
||||
gui.State.Panels.ReflogCommits.SelectedLine = 0
|
||||
|
||||
return gui.refreshSidePanels(gui.g)
|
||||
}
|
||||
}
|
||||
|
||||
options := []*reflogResetOption{
|
||||
{
|
||||
description: gui.Tr.SLocalize("hardReset"),
|
||||
command: fmt.Sprintf("reset --hard %s", commit.Sha),
|
||||
handler: resetFunction(gui.GitCommand.ResetHard),
|
||||
},
|
||||
{
|
||||
description: gui.Tr.SLocalize("softReset"),
|
||||
command: fmt.Sprintf("reset --soft %s", commit.Sha),
|
||||
handler: resetFunction(gui.GitCommand.ResetSoft),
|
||||
},
|
||||
{
|
||||
description: gui.Tr.SLocalize("cancel"),
|
||||
handler: func() error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
handleMenuPress := func(index int) error {
|
||||
return options[index].handler()
|
||||
}
|
||||
|
||||
return gui.createMenu("", options, len(options), handleMenuPress)
|
||||
}
|
||||
@@ -2,12 +2,10 @@ package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
)
|
||||
|
||||
// list panel functions
|
||||
@@ -34,24 +32,23 @@ func (gui *Gui) handleRemoteBranchSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
|
||||
gui.getMainView().Title = "Remote Branch"
|
||||
|
||||
remote := gui.getSelectedRemote()
|
||||
remoteBranch := gui.getSelectedRemoteBranch()
|
||||
if remoteBranch == nil {
|
||||
return gui.renderString(g, "main", "No branches for this remote")
|
||||
return gui.newStringTask("main", "No branches for this remote")
|
||||
}
|
||||
|
||||
gui.focusPoint(0, gui.State.Panels.Menu.SelectedLine, gui.State.MenuItemCount, v)
|
||||
if err := gui.focusPoint(0, gui.State.Panels.RemoteBranches.SelectedLine, len(gui.State.RemoteBranches), v); err != nil {
|
||||
return err
|
||||
v.FocusPoint(0, gui.State.Panels.RemoteBranches.SelectedLine)
|
||||
|
||||
if gui.inDiffMode() {
|
||||
return gui.renderDiff()
|
||||
}
|
||||
|
||||
go func() {
|
||||
graph, err := gui.GitCommand.GetBranchGraph(fmt.Sprintf("%s/%s", remote.Name, remoteBranch.Name))
|
||||
if err != nil && strings.HasPrefix(graph, "fatal: ambiguous argument") {
|
||||
graph = gui.Tr.SLocalize("NoTrackingThisBranch")
|
||||
}
|
||||
_ = gui.renderString(g, "main", fmt.Sprintf("%s/%s\n\n%s", utils.ColoredString(remote.Name, color.FgRed), utils.ColoredString(remoteBranch.Name, color.FgGreen), graph))
|
||||
}()
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.GetBranchGraphCmdStr(remoteBranch.FullName()),
|
||||
)
|
||||
if err := gui.newCmdTask("main", cmd); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -64,9 +61,8 @@ func (gui *Gui) renderRemoteBranchesWithSelection() error {
|
||||
branchesView := gui.getBranchesView()
|
||||
|
||||
gui.refreshSelectedLine(&gui.State.Panels.RemoteBranches.SelectedLine, len(gui.State.RemoteBranches))
|
||||
if err := gui.renderListPanel(branchesView, gui.State.RemoteBranches); err != nil {
|
||||
return err
|
||||
}
|
||||
displayStrings := presentation.GetRemoteBranchListDisplayStrings(gui.State.RemoteBranches, gui.State.Diff.Ref)
|
||||
gui.renderDisplayStrings(branchesView, displayStrings)
|
||||
if gui.g.CurrentView() == branchesView && branchesView.Context == "remote-branches" {
|
||||
if err := gui.handleRemoteBranchSelect(gui.g, branchesView); err != nil {
|
||||
return err
|
||||
@@ -81,14 +77,14 @@ func (gui *Gui) handleCheckoutRemoteBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
if remoteBranch == nil {
|
||||
return nil
|
||||
}
|
||||
if err := gui.handleCheckoutRef(remoteBranch.RemoteName + "/" + remoteBranch.Name); err != nil {
|
||||
if err := gui.handleCheckoutRef(remoteBranch.FullName(), handleCheckoutRefOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.switchBranchesPanelContext("local-branches")
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMergeRemoteBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
selectedBranchName := gui.getSelectedRemoteBranch().Name
|
||||
selectedBranchName := gui.getSelectedRemoteBranch().FullName()
|
||||
return gui.mergeBranchIntoCheckedOutBranch(selectedBranchName)
|
||||
}
|
||||
|
||||
@@ -104,7 +100,7 @@ func (gui *Gui) handleDeleteRemoteBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.refreshRemotes()
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
|
||||
})
|
||||
}, nil)
|
||||
}
|
||||
@@ -122,7 +118,7 @@ func (gui *Gui) handleSetBranchUpstream(g *gocui.Gui, v *gocui.View) error {
|
||||
"SetUpstreamMessage",
|
||||
Teml{
|
||||
"checkedOut": checkedOutBranch.Name,
|
||||
"selected": selectedBranch.RemoteName + "/" + selectedBranch.Name,
|
||||
"selected": selectedBranch.FullName(),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -131,6 +127,38 @@ func (gui *Gui) handleSetBranchUpstream(g *gocui.Gui, v *gocui.View) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(gui.g)
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateResetToRemoteBranchMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
selectedBranch := gui.getSelectedRemoteBranch()
|
||||
if selectedBranch == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.createResetMenu(fmt.Sprintf("%s/%s", selectedBranch.RemoteName, selectedBranch.Name))
|
||||
}
|
||||
|
||||
func (gui *Gui) handleNewBranchOffRemote(g *gocui.Gui, v *gocui.View) error {
|
||||
branch := gui.getSelectedRemoteBranch()
|
||||
if branch == nil {
|
||||
return nil
|
||||
}
|
||||
message := gui.Tr.TemplateLocalize(
|
||||
"NewBranchNameBranchOff",
|
||||
Teml{
|
||||
"branchName": branch.FullName(),
|
||||
},
|
||||
)
|
||||
return gui.createPromptPanel(g, v, message, branch.FullName(), func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.NewBranch(gui.trimmedContent(v), branch.FullName()); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
gui.State.Panels.Branches.SelectedLine = 0
|
||||
if err := gui.switchBranchesPanelContext("local-branches"); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -36,13 +37,15 @@ func (gui *Gui) handleRemoteSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
|
||||
remote := gui.getSelectedRemote()
|
||||
if remote == nil {
|
||||
return gui.renderString(g, "main", "No remotes")
|
||||
return gui.newStringTask("main", "No remotes")
|
||||
}
|
||||
if err := gui.focusPoint(0, gui.State.Panels.Remotes.SelectedLine, len(gui.State.Remotes), v); err != nil {
|
||||
return err
|
||||
v.FocusPoint(0, gui.State.Panels.Remotes.SelectedLine)
|
||||
|
||||
if gui.inDiffMode() {
|
||||
return gui.renderDiff()
|
||||
}
|
||||
|
||||
return gui.renderString(g, "main", fmt.Sprintf("%s\nUrls:\n%s", utils.ColoredString(remote.Name, color.FgGreen), strings.Join(remote.Urls, "\n")))
|
||||
return gui.newStringTask("main", fmt.Sprintf("%s\nUrls:\n%s", utils.ColoredString(remote.Name, color.FgGreen), strings.Join(remote.Urls, "\n")))
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshRemotes() error {
|
||||
@@ -50,7 +53,7 @@ func (gui *Gui) refreshRemotes() error {
|
||||
|
||||
remotes, err := gui.GitCommand.GetRemotes()
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
gui.State.Remotes = remotes
|
||||
@@ -80,9 +83,10 @@ func (gui *Gui) renderRemotesWithSelection() error {
|
||||
branchesView := gui.getBranchesView()
|
||||
|
||||
gui.refreshSelectedLine(&gui.State.Panels.Remotes.SelectedLine, len(gui.State.Remotes))
|
||||
if err := gui.renderListPanel(branchesView, gui.State.Remotes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
displayStrings := presentation.GetRemoteListDisplayStrings(gui.State.Remotes, gui.State.Diff.Ref)
|
||||
gui.renderDisplayStrings(branchesView, displayStrings)
|
||||
|
||||
if gui.g.CurrentView() == branchesView && branchesView.Context == "remotes" {
|
||||
if err := gui.handleRemoteSelect(gui.g, branchesView); err != nil {
|
||||
return err
|
||||
@@ -119,7 +123,7 @@ func (gui *Gui) handleAddRemote(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.AddRemote(remoteName, remoteUrl); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.refreshRemotes()
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{REMOTES}})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -134,7 +138,7 @@ func (gui *Gui) handleRemoveRemote(g *gocui.Gui, v *gocui.View) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.refreshRemotes()
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
|
||||
|
||||
}, nil)
|
||||
}
|
||||
@@ -158,7 +162,7 @@ func (gui *Gui) handleEditRemote(g *gocui.Gui, v *gocui.View) error {
|
||||
|
||||
if updatedRemoteName != remote.Name {
|
||||
if err := gui.GitCommand.RenameRemote(remote.Name, updatedRemoteName); err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,9 +176,9 @@ func (gui *Gui) handleEditRemote(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.createPromptPanel(g, branchesView, editUrlMessage, "", func(g *gocui.Gui, v *gocui.View) error {
|
||||
updatedRemoteUrl := gui.trimmedContent(v)
|
||||
if err := gui.GitCommand.UpdateRemoteUrl(updatedRemoteName, updatedRemoteUrl); err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshRemotes()
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -190,6 +194,6 @@ func (gui *Gui) handleFetchRemote(g *gocui.Gui, v *gocui.View) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.refreshRemotes()
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
|
||||
})
|
||||
}
|
||||
|
||||
53
pkg/gui/reset_menu_panel.go
Normal file
53
pkg/gui/reset_menu_panel.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
)
|
||||
|
||||
func (gui *Gui) resetToRef(ref string, strength string, options commands.RunCommandOptions) error {
|
||||
if err := gui.GitCommand.ResetToCommit(ref, strength, options); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
if err := gui.switchCommitsPanelContext("branch-commits"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.State.Panels.Commits.SelectedLine = 0
|
||||
gui.State.Panels.ReflogCommits.SelectedLine = 0
|
||||
// loading a heap of commits is slow so we limit them whenever doing a reset
|
||||
gui.State.Panels.Commits.LimitCommits = true
|
||||
|
||||
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES, BRANCHES, REFLOG, COMMITS}}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.resetOrigin(gui.getCommitsView()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.handleCommitSelect(gui.g, gui.getCommitsView())
|
||||
}
|
||||
|
||||
func (gui *Gui) createResetMenu(ref string) error {
|
||||
strengths := []string{"soft", "mixed", "hard"}
|
||||
menuItems := make([]*menuItem, len(strengths))
|
||||
for i, strength := range strengths {
|
||||
strength := strength
|
||||
menuItems[i] = &menuItem{
|
||||
displayStrings: []string{
|
||||
fmt.Sprintf("%s reset", strength),
|
||||
color.New(color.FgRed).Sprint(
|
||||
fmt.Sprintf("reset --%s %s", strength, ref),
|
||||
),
|
||||
},
|
||||
onPress: func() error {
|
||||
return gui.resetToRef(ref, strength, commands.RunCommandOptions{})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return gui.createMenu(fmt.Sprintf("%s %s", gui.Tr.SLocalize("resetTo"), ref), menuItems, createMenuOptions{showCancel: true})
|
||||
}
|
||||
94
pkg/gui/searching.go
Normal file
94
pkg/gui/searching.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func (gui *Gui) handleOpenSearch(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.State.Searching.isSearching = true
|
||||
gui.State.Searching.view = v
|
||||
gui.renderString(gui.g, "search", "")
|
||||
if err := gui.switchFocus(gui.g, v, gui.getSearchView()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSearch(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.State.Searching.searchString = gui.getSearchView().Buffer()
|
||||
if err := gui.switchFocus(gui.g, nil, gui.State.Searching.view); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.State.Searching.view.Search(gui.State.Searching.searchString); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) onSelectItemWrapper(innerFunc func(int) error) func(int, int, int) error {
|
||||
return func(y int, index int, total int) error {
|
||||
if total == 0 {
|
||||
gui.renderString(
|
||||
gui.g,
|
||||
"search",
|
||||
fmt.Sprintf(
|
||||
"no matches for '%s' %s",
|
||||
gui.State.Searching.searchString,
|
||||
utils.ColoredString(
|
||||
fmt.Sprintf("%s: exit search mode", gui.getKeyDisplay("universal.return")),
|
||||
theme.OptionsFgColor,
|
||||
),
|
||||
),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
gui.renderString(
|
||||
gui.g,
|
||||
"search",
|
||||
fmt.Sprintf(
|
||||
"matches for '%s' (%d of %d) %s",
|
||||
gui.State.Searching.searchString,
|
||||
index+1,
|
||||
total,
|
||||
utils.ColoredString(
|
||||
fmt.Sprintf(
|
||||
"%s: next match, %s: previous match, %s: exit search mode",
|
||||
gui.getKeyDisplay("universal.nextMatch"),
|
||||
gui.getKeyDisplay("universal.prevMatch"),
|
||||
gui.getKeyDisplay("universal.return"),
|
||||
),
|
||||
theme.OptionsFgColor,
|
||||
),
|
||||
),
|
||||
)
|
||||
if err := innerFunc(y); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) onSearchEscape() error {
|
||||
gui.State.Searching.isSearching = false
|
||||
if gui.State.Searching.view != nil {
|
||||
gui.State.Searching.view.ClearSearch()
|
||||
gui.State.Searching.view = nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSearchEscape(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.switchFocus(gui.g, nil, gui.State.Searching.view); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.onSearchEscape()
|
||||
}
|
||||
@@ -12,7 +12,14 @@ func (gui *Gui) refreshStagingPanel(forceSecondaryFocused bool, selectedLineIdx
|
||||
|
||||
state := gui.State.Panels.LineByLine
|
||||
|
||||
file, err := gui.getSelectedFile(gui.g)
|
||||
// We need to force focus here because the confirmation panel for safely staging lines does not return focus automatically.
|
||||
// This is because if we tell it to return focus it will unconditionally return it to the main panel which may not be what we want
|
||||
// e.g. in the event that there's nothing left to stage.
|
||||
if err := gui.switchFocus(gui.g, nil, gui.getMainView()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := gui.getSelectedFile()
|
||||
if err != nil {
|
||||
if err != gui.Errors.ErrNoFiles {
|
||||
return err
|
||||
@@ -87,27 +94,38 @@ func (gui *Gui) handleTogglePanel(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStagingEscape(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.State.Panels.LineByLine = nil
|
||||
gui.handleEscapeLineByLinePanel()
|
||||
|
||||
return gui.switchFocus(gui.g, nil, gui.getFilesView())
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStageSelection(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.applySelection(false)
|
||||
func (gui *Gui) handleToggleStagedSelection(g *gocui.Gui, v *gocui.View) error {
|
||||
state := gui.State.Panels.LineByLine
|
||||
|
||||
return gui.applySelection(state.SecondaryFocused)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleResetSelection(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.applySelection(true)
|
||||
state := gui.State.Panels.LineByLine
|
||||
|
||||
if state.SecondaryFocused {
|
||||
// for backwards compatibility
|
||||
return gui.applySelection(true)
|
||||
}
|
||||
|
||||
if !gui.Config.GetUserConfig().GetBool("gui.skipUnstageLineWarning") {
|
||||
return gui.createConfirmationPanel(gui.g, gui.getMainView(), false, "unstage lines", "Are you sure you want to delete the selected lines (git reset)? It is irreversible.\nTo disable this dialogue set the config key of 'gui.skipUnstageLineWarning' to true", func(*gocui.Gui, *gocui.View) error {
|
||||
return gui.applySelection(true)
|
||||
}, nil)
|
||||
} else {
|
||||
return gui.applySelection(true)
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) applySelection(reverse bool) error {
|
||||
state := gui.State.Panels.LineByLine
|
||||
|
||||
if !reverse && state.SecondaryFocused {
|
||||
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("CantStageStaged"))
|
||||
}
|
||||
|
||||
file, err := gui.getSelectedFile(gui.g)
|
||||
file, err := gui.getSelectedFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -126,14 +144,14 @@ func (gui *Gui) applySelection(reverse bool) error {
|
||||
}
|
||||
err = gui.GitCommand.ApplyPatch(patch, applyFlags...)
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
if state.SelectMode == RANGE {
|
||||
state.SelectMode = LINE
|
||||
}
|
||||
|
||||
if err := gui.refreshFiles(); err != nil {
|
||||
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.refreshStagingPanel(false, -1); err != nil {
|
||||
@@ -141,11 +159,3 @@ func (gui *Gui) applySelection(reverse bool) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMouseDownSecondaryWhileStaging(g *gocui.Gui, v *gocui.View) error {
|
||||
state := gui.State.Panels.LineByLine
|
||||
|
||||
state.SecondaryFocused = !state.SecondaryFocused
|
||||
|
||||
return gui.refreshStagingPanel(false, -1)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
)
|
||||
|
||||
// list panel functions
|
||||
|
||||
func (gui *Gui) getSelectedStashEntry(v *gocui.View) *commands.StashEntry {
|
||||
func (gui *Gui) getSelectedStashEntry() *commands.StashEntry {
|
||||
selectedLine := gui.State.Panels.Stash.SelectedLine
|
||||
if selectedLine == -1 {
|
||||
return nil
|
||||
@@ -32,53 +30,75 @@ func (gui *Gui) handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error {
|
||||
|
||||
gui.getMainView().Title = "Stash"
|
||||
|
||||
stashEntry := gui.getSelectedStashEntry(v)
|
||||
stashEntry := gui.getSelectedStashEntry()
|
||||
if stashEntry == nil {
|
||||
return gui.renderString(g, "main", gui.Tr.SLocalize("NoStashEntries"))
|
||||
return gui.newStringTask("main", gui.Tr.SLocalize("NoStashEntries"))
|
||||
}
|
||||
if err := gui.focusPoint(0, gui.State.Panels.Stash.SelectedLine, len(gui.State.StashEntries), v); err != nil {
|
||||
return err
|
||||
v.FocusPoint(0, gui.State.Panels.Stash.SelectedLine)
|
||||
|
||||
if gui.inDiffMode() {
|
||||
return gui.renderDiff()
|
||||
}
|
||||
go func() {
|
||||
// doing this asynchronously cos it can take time
|
||||
diff, _ := gui.GitCommand.GetStashEntryDiff(stashEntry.Index)
|
||||
_ = gui.renderString(g, "main", diff)
|
||||
}()
|
||||
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.ShowStashEntryCmdStr(stashEntry.Index),
|
||||
)
|
||||
if err := gui.newPtyTask("main", cmd); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshStashEntries(g *gocui.Gui) error {
|
||||
g.Update(func(g *gocui.Gui) error {
|
||||
gui.State.StashEntries = gui.GitCommand.GetStashEntries()
|
||||
gui.State.StashEntries = gui.GitCommand.GetStashEntries(gui.State.FilterPath)
|
||||
|
||||
gui.refreshSelectedLine(&gui.State.Panels.Stash.SelectedLine, len(gui.State.StashEntries))
|
||||
gui.refreshSelectedLine(&gui.State.Panels.Stash.SelectedLine, len(gui.State.StashEntries))
|
||||
|
||||
isFocused := gui.g.CurrentView().Name() == "stash"
|
||||
list, err := utils.RenderList(gui.State.StashEntries, isFocused)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stashView := gui.getStashView()
|
||||
|
||||
v := gui.getStashView()
|
||||
v.Clear()
|
||||
fmt.Fprint(v, list)
|
||||
displayStrings := presentation.GetStashEntryListDisplayStrings(gui.State.StashEntries, gui.State.Diff.Ref)
|
||||
gui.renderDisplayStrings(stashView, displayStrings)
|
||||
|
||||
if err := gui.resetOrigin(v); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
return gui.resetOrigin(stashView)
|
||||
}
|
||||
|
||||
// specific functions
|
||||
|
||||
func (gui *Gui) handleStashApply(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.stashDo(g, v, "apply")
|
||||
skipStashWarning := gui.Config.GetUserConfig().GetBool("gui.skipStashWarning")
|
||||
|
||||
apply := func() error {
|
||||
return gui.stashDo(g, v, "apply")
|
||||
}
|
||||
|
||||
if skipStashWarning {
|
||||
return apply()
|
||||
}
|
||||
|
||||
title := gui.Tr.SLocalize("StashApply")
|
||||
message := gui.Tr.SLocalize("SureApplyStashEntry")
|
||||
return gui.createConfirmationPanel(g, v, true, title, message, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return apply()
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStashPop(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.stashDo(g, v, "pop")
|
||||
skipStashWarning := gui.Config.GetUserConfig().GetBool("gui.skipStashWarning")
|
||||
|
||||
pop := func() error {
|
||||
return gui.stashDo(g, v, "pop")
|
||||
}
|
||||
|
||||
if skipStashWarning {
|
||||
return pop()
|
||||
}
|
||||
|
||||
title := gui.Tr.SLocalize("StashPop")
|
||||
message := gui.Tr.SLocalize("SurePopStashEntry")
|
||||
return gui.createConfirmationPanel(g, v, true, title, message, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return pop()
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStashDrop(g *gocui.Gui, v *gocui.View) error {
|
||||
@@ -90,7 +110,7 @@ func (gui *Gui) handleStashDrop(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) stashDo(g *gocui.Gui, v *gocui.View, method string) error {
|
||||
stashEntry := gui.getSelectedStashEntry(v)
|
||||
stashEntry := gui.getSelectedStashEntry()
|
||||
if stashEntry == nil {
|
||||
errorMessage := gui.Tr.TemplateLocalize(
|
||||
"NoStashTo",
|
||||
@@ -98,24 +118,27 @@ func (gui *Gui) stashDo(g *gocui.Gui, v *gocui.View, method string) error {
|
||||
"method": method,
|
||||
},
|
||||
)
|
||||
return gui.createErrorPanel(g, errorMessage)
|
||||
return gui.createErrorPanel(errorMessage)
|
||||
}
|
||||
if err := gui.GitCommand.StashDo(stashEntry.Index, method); err != nil {
|
||||
gui.createErrorPanel(g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
gui.refreshStashEntries(g)
|
||||
return gui.refreshFiles()
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{STASH, FILES}})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStashSave(stashFunc func(message string) error) error {
|
||||
if len(gui.trackedFiles()) == 0 && len(gui.stagedFiles()) == 0 {
|
||||
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NoTrackedStagedFilesStash"))
|
||||
return gui.createErrorPanel(gui.Tr.SLocalize("NoTrackedStagedFilesStash"))
|
||||
}
|
||||
return gui.createPromptPanel(gui.g, gui.getFilesView(), gui.Tr.SLocalize("StashChanges"), "", func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := stashFunc(gui.trimmedContent(v)); err != nil {
|
||||
gui.createErrorPanel(g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
gui.refreshStashEntries(g)
|
||||
return gui.refreshFiles()
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{STASH, FILES}})
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) onStashPanelSearchSelect(selectedLine int) error {
|
||||
gui.State.Panels.Stash.SelectedLine = selectedLine
|
||||
return gui.handleStashEntrySelect(gui.g, gui.getStashView())
|
||||
}
|
||||
|
||||
@@ -6,45 +6,45 @@ import (
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func (gui *Gui) refreshStatus(g *gocui.Gui) error {
|
||||
state := gui.State.Panels.Status
|
||||
// never call this on its own, it should only be called from within refreshCommits()
|
||||
func (gui *Gui) refreshStatus() {
|
||||
gui.State.RefreshingStatusMutex.Lock()
|
||||
defer gui.State.RefreshingStatusMutex.Unlock()
|
||||
|
||||
v, err := g.View("status")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
currentBranch := gui.currentBranch()
|
||||
if currentBranch == nil {
|
||||
// need to wait for branches to refresh
|
||||
return
|
||||
}
|
||||
// for some reason if this isn't wrapped in an update the clear seems to
|
||||
// be applied after the other things or something like that; the panel's
|
||||
// contents end up cleared
|
||||
g.Update(func(*gocui.Gui) error {
|
||||
v.Clear()
|
||||
state.pushables, state.pullables = gui.GitCommand.GetCurrentBranchUpstreamDifferenceCount()
|
||||
if err := gui.updateWorkTreeState(); err != nil {
|
||||
return err
|
||||
}
|
||||
status := fmt.Sprintf("↑%s↓%s", state.pushables, state.pullables)
|
||||
branches := gui.State.Branches
|
||||
status := ""
|
||||
|
||||
if gui.State.WorkingTreeState != "normal" {
|
||||
status += utils.ColoredString(fmt.Sprintf(" (%s)", gui.State.WorkingTreeState), color.FgYellow)
|
||||
if currentBranch.Pushables != "" && currentBranch.Pullables != "" {
|
||||
trackColor := color.FgYellow
|
||||
if currentBranch.Pushables == "0" && currentBranch.Pullables == "0" {
|
||||
trackColor = color.FgGreen
|
||||
} else if currentBranch.Pushables == "?" && currentBranch.Pullables == "?" {
|
||||
trackColor = color.FgRed
|
||||
}
|
||||
|
||||
if len(branches) > 0 {
|
||||
branch := branches[0]
|
||||
name := utils.ColoredString(branch.Name, commands.GetBranchColor(branch.Name))
|
||||
repoName := utils.GetCurrentRepoName()
|
||||
status += fmt.Sprintf(" %s → %s", repoName, name)
|
||||
}
|
||||
status = utils.ColoredString(fmt.Sprintf("↑%s↓%s ", currentBranch.Pushables, currentBranch.Pullables), trackColor)
|
||||
}
|
||||
|
||||
fmt.Fprint(v, status)
|
||||
if gui.GitCommand.WorkingTreeState() != "normal" {
|
||||
status += utils.ColoredString(fmt.Sprintf("(%s) ", gui.GitCommand.WorkingTreeState()), color.FgYellow)
|
||||
}
|
||||
|
||||
name := utils.ColoredString(currentBranch.Name, presentation.GetBranchColor(currentBranch.Name))
|
||||
repoName := utils.GetCurrentRepoName()
|
||||
status += fmt.Sprintf("%s → %s ", repoName, name)
|
||||
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
gui.setViewContent(gui.getStatusView(), status)
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runeCount(str string) int {
|
||||
@@ -61,15 +61,14 @@ func (gui *Gui) handleCheckForUpdate(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStatusClick(g *gocui.Gui, v *gocui.View) error {
|
||||
state := gui.State.Panels.Status
|
||||
currentBranch := gui.currentBranch()
|
||||
|
||||
cx, _ := v.Cursor()
|
||||
upstreamStatus := fmt.Sprintf("↑%s↓%s", state.pushables, state.pullables)
|
||||
upstreamStatus := fmt.Sprintf("↑%s↓%s", currentBranch.Pushables, currentBranch.Pullables)
|
||||
repoName := utils.GetCurrentRepoName()
|
||||
gui.Log.Warn(gui.State.WorkingTreeState)
|
||||
switch gui.State.WorkingTreeState {
|
||||
switch gui.GitCommand.WorkingTreeState() {
|
||||
case "rebasing", "merging":
|
||||
workingTreeStatus := fmt.Sprintf("(%s)", gui.State.WorkingTreeState)
|
||||
workingTreeStatus := fmt.Sprintf("(%s)", gui.GitCommand.WorkingTreeState())
|
||||
if cursorInSubstring(cx, upstreamStatus+" ", workingTreeStatus) {
|
||||
return gui.handleCreateRebaseOptionsMenu(gui.g, v)
|
||||
}
|
||||
@@ -98,6 +97,10 @@ func (gui *Gui) handleStatusSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
|
||||
gui.getMainView().Title = ""
|
||||
|
||||
if gui.inDiffMode() {
|
||||
return gui.renderDiff()
|
||||
}
|
||||
|
||||
magenta := color.New(color.FgMagenta)
|
||||
|
||||
dashboardString := strings.Join(
|
||||
@@ -111,7 +114,7 @@ func (gui *Gui) handleStatusSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
magenta.Sprint("Become a sponsor (github is matching all donations for 12 months): https://github.com/sponsors/jesseduffield"), // caffeine ain't free
|
||||
}, "\n\n")
|
||||
|
||||
return gui.renderString(g, "main", dashboardString)
|
||||
return gui.newStringTask("main", dashboardString)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleOpenConfig(g *gocui.Gui, v *gocui.View) error {
|
||||
@@ -135,23 +138,14 @@ func lazygitTitle() string {
|
||||
|___/ |___/ `
|
||||
}
|
||||
|
||||
func (gui *Gui) updateWorkTreeState() error {
|
||||
merging, err := gui.GitCommand.IsInMergeState()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if merging {
|
||||
gui.State.WorkingTreeState = "merging"
|
||||
return nil
|
||||
}
|
||||
rebaseMode, err := gui.GitCommand.RebaseMode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
func (gui *Gui) workingTreeState() string {
|
||||
rebaseMode, _ := gui.GitCommand.RebaseMode()
|
||||
if rebaseMode != "" {
|
||||
gui.State.WorkingTreeState = "rebasing"
|
||||
return nil
|
||||
return "rebasing"
|
||||
}
|
||||
gui.State.WorkingTreeState = "normal"
|
||||
return nil
|
||||
merging, _ := gui.GitCommand.IsInMergeState()
|
||||
if merging {
|
||||
return "merging"
|
||||
}
|
||||
return "normal"
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
)
|
||||
|
||||
// list panel functions
|
||||
@@ -33,25 +32,20 @@ func (gui *Gui) handleTagSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
|
||||
tag := gui.getSelectedTag()
|
||||
if tag == nil {
|
||||
return gui.renderString(g, "main", "No tags")
|
||||
return gui.newStringTask("main", "No tags")
|
||||
}
|
||||
if err := gui.focusPoint(0, gui.State.Panels.Tags.SelectedLine, len(gui.State.Tags), v); err != nil {
|
||||
return err
|
||||
v.FocusPoint(0, gui.State.Panels.Tags.SelectedLine)
|
||||
|
||||
if gui.inDiffMode() {
|
||||
return gui.renderDiff()
|
||||
}
|
||||
|
||||
go func() {
|
||||
show, err := gui.GitCommand.ShowTag(tag.Name)
|
||||
if err != nil {
|
||||
show = ""
|
||||
}
|
||||
|
||||
graph, err := gui.GitCommand.GetBranchGraph(tag.Name)
|
||||
if err != nil {
|
||||
graph = "No graph for tag " + tag.Name
|
||||
}
|
||||
|
||||
_ = gui.renderString(g, "main", fmt.Sprintf("%s\n%s", show, graph))
|
||||
}()
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.GetBranchGraphCmdStr(tag.Name),
|
||||
)
|
||||
if err := gui.newCmdTask("main", cmd); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -59,13 +53,13 @@ func (gui *Gui) handleTagSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) refreshTags() error {
|
||||
tags, err := gui.GitCommand.GetTags()
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
gui.State.Tags = tags
|
||||
|
||||
if gui.getBranchesView().Context == "tags" {
|
||||
gui.renderTagsWithSelection()
|
||||
return gui.renderTagsWithSelection()
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -75,12 +69,11 @@ func (gui *Gui) renderTagsWithSelection() error {
|
||||
branchesView := gui.getBranchesView()
|
||||
|
||||
gui.refreshSelectedLine(&gui.State.Panels.Tags.SelectedLine, len(gui.State.Tags))
|
||||
if err := gui.renderListPanel(branchesView, gui.State.Tags); err != nil {
|
||||
return err
|
||||
}
|
||||
displayStrings := presentation.GetTagListDisplayStrings(gui.State.Tags, gui.State.Diff.Ref)
|
||||
gui.renderDisplayStrings(branchesView, displayStrings)
|
||||
if gui.g.CurrentView() == branchesView && branchesView.Context == "tags" {
|
||||
if err := gui.handleTagSelect(gui.g, branchesView); err != nil {
|
||||
return err
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +85,7 @@ func (gui *Gui) handleCheckoutTag(g *gocui.Gui, v *gocui.View) error {
|
||||
if tag == nil {
|
||||
return nil
|
||||
}
|
||||
if err := gui.handleCheckoutRef(tag.Name); err != nil {
|
||||
if err := gui.handleCheckoutRef(tag.Name, handleCheckoutRefOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.switchBranchesPanelContext("local-branches")
|
||||
@@ -113,9 +106,9 @@ func (gui *Gui) handleDeleteTag(g *gocui.Gui, v *gocui.View) error {
|
||||
|
||||
return gui.createConfirmationPanel(gui.g, v, true, gui.Tr.SLocalize("DeleteTagTitle"), prompt, func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.DeleteTag(tag.Name); err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshTags()
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{COMMITS, TAGS}})
|
||||
}, nil)
|
||||
}
|
||||
|
||||
@@ -134,18 +127,38 @@ func (gui *Gui) handlePushTag(g *gocui.Gui, v *gocui.View) error {
|
||||
|
||||
return gui.createPromptPanel(gui.g, v, title, "origin", func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.PushTag(v.Buffer(), tag.Name); err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshTags()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateTag(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.createPromptPanel(gui.g, v, gui.Tr.SLocalize("CreateTagTitle"), "", func(g *gocui.Gui, v *gocui.View) error {
|
||||
// leaving commit SHA blank so that we're just creating the tag for the current commit
|
||||
if err := gui.GitCommand.CreateLightweightTag(v.Buffer(), ""); err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
tagName := v.Buffer()
|
||||
if err := gui.GitCommand.CreateLightweightTag(tagName, ""); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshTags()
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []int{COMMITS, TAGS}, then: func() {
|
||||
// find the index of the tag and set that as the currently selected line
|
||||
for i, tag := range gui.State.Tags {
|
||||
if tag.Name == tagName {
|
||||
gui.State.Panels.Tags.SelectedLine = i
|
||||
gui.renderTagsWithSelection()
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateResetToTagMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
tag := gui.getSelectedTag()
|
||||
if tag == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.createResetMenu(tag.Name)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user