mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-28 18:26:03 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8287659584 | ||
|
|
cf95ab9a28 | ||
|
|
b907c74386 | ||
|
|
a68fb4fb8f | ||
|
|
945fccd211 | ||
|
|
12b84307ac | ||
|
|
6843741d9e | ||
|
|
945edb253b | ||
|
|
cbc82cd3c1 | ||
|
|
29ee239987 | ||
|
|
3f7e107d09 | ||
|
|
22c0d79e2d | ||
|
|
e174e5254d | ||
|
|
de5bcb8b9c | ||
|
|
98666186ee | ||
|
|
941d3c6648 | ||
|
|
5c518eda0a | ||
|
|
df72eee201 | ||
|
|
131113b065 | ||
|
|
e85310c0a9 | ||
|
|
cd17b46b55 | ||
|
|
d0d92c7697 | ||
|
|
89a9b4e6d5 | ||
|
|
592a2ff196 | ||
|
|
17ed90c790 | ||
|
|
9e0b335669 | ||
|
|
194c554357 | ||
|
|
c65790b53d | ||
|
|
2f37c0caaf | ||
|
|
86a39e3aea | ||
|
|
326b1ca8c9 | ||
|
|
72fe770974 | ||
|
|
db8c398fa3 | ||
|
|
861bcc38be | ||
|
|
cd3874ffb7 | ||
|
|
10fe88a2cf | ||
|
|
1a38bfb76d | ||
|
|
beaebb7dc7 | ||
|
|
6d5d054c30 | ||
|
|
2344155379 | ||
|
|
48347d4d86 | ||
|
|
61deaaddb7 | ||
|
|
0046e9c469 | ||
|
|
733145d132 | ||
|
|
f285d80d0e | ||
|
|
0ffccbd3ee | ||
|
|
1fc120de2d | ||
|
|
d5e443e8e3 | ||
|
|
a3c84296bf | ||
|
|
cc039d1f9b | ||
|
|
2484ec9c11 | ||
|
|
5f9de1f034 | ||
|
|
66eaaf9cbb | ||
|
|
87ac193b5e | ||
|
|
11e57edbb3 | ||
|
|
4f2c42ea47 | ||
|
|
820f3d5cbb | ||
|
|
081598d989 | ||
|
|
09f268befc | ||
|
|
4bc974c83c | ||
|
|
63da8f48da | ||
|
|
32d6a17240 | ||
|
|
84d869a3a0 | ||
|
|
a1c6619401 | ||
|
|
3524f6baa9 | ||
|
|
ac5cbc1d2c | ||
|
|
a045313e08 | ||
|
|
9bd2dc3050 | ||
|
|
02fef3136f | ||
|
|
8fe0e00cd9 | ||
|
|
f7f19bbc02 | ||
|
|
95ae806e09 | ||
|
|
d8a6f173c3 | ||
|
|
431f1aa766 | ||
|
|
379dcf0972 | ||
|
|
0d25d113c9 | ||
|
|
7c70913e8d | ||
|
|
1c5858c515 | ||
|
|
c3767bb3b3 | ||
|
|
b92d27ee7f | ||
|
|
6eff139c40 | ||
|
|
07462303ab | ||
|
|
d12f81b44e | ||
|
|
600112780c | ||
|
|
4c73c8889f | ||
|
|
68d5c2bc10 | ||
|
|
7db1fee877 | ||
|
|
8f786e3fd9 | ||
|
|
1c704e11f2 | ||
|
|
e0dd1cb29d | ||
|
|
827837b0b9 | ||
|
|
e83ef9858b | ||
|
|
504d506575 | ||
|
|
823b436b53 | ||
|
|
212327d746 | ||
|
|
cc138fc70e | ||
|
|
d953712377 | ||
|
|
69ac0036e6 | ||
|
|
975a5315b0 | ||
|
|
8f734b11e3 | ||
|
|
17b4cabc71 | ||
|
|
75db4faf69 | ||
|
|
e1f5601d4b | ||
|
|
b60ecdaa24 | ||
|
|
9fb9962ce7 | ||
|
|
25c93c678d | ||
|
|
b8baef7b8f | ||
|
|
235a47bb80 | ||
|
|
c107eed890 | ||
|
|
abddea060e | ||
|
|
3e40369fd2 | ||
|
|
0f0fda1660 | ||
|
|
bd2170a99c | ||
|
|
c039e5bed0 | ||
|
|
527c025a0c | ||
|
|
53cded77f1 | ||
|
|
4a4dc676fc | ||
|
|
c61bfbdd4c | ||
|
|
e38d9d5f22 | ||
|
|
97f060d38d | ||
|
|
357b8fa98f | ||
|
|
8754d766e2 | ||
|
|
2388c3ee9a | ||
|
|
61890cb9de | ||
|
|
5a0d0bb299 | ||
|
|
2746b1bd38 | ||
|
|
e09aac6450 | ||
|
|
19a6368377 | ||
|
|
e6122122e9 | ||
|
|
492614ebc7 | ||
|
|
d31f0ed39b | ||
|
|
b505c295d2 | ||
|
|
0b9d7edd47 | ||
|
|
e9fbb608a8 | ||
|
|
6ba05c94ea | ||
|
|
07fec6d00e | ||
|
|
a69b985086 | ||
|
|
471733fe03 | ||
|
|
0d3a193ab5 | ||
|
|
ab9fa291a8 | ||
|
|
cadc74eeec | ||
|
|
fc3a57b5e2 | ||
|
|
7ff07e1454 | ||
|
|
3e779bca8d | ||
|
|
0f1abcb10c | ||
|
|
55538a3695 | ||
|
|
878a15aff4 | ||
|
|
60e33f5d8c | ||
|
|
b422692746 | ||
|
|
f34be1896a | ||
|
|
c350cdba43 | ||
|
|
1a933eaa73 | ||
|
|
bd46e9c5b0 | ||
|
|
09b7ae21bc | ||
|
|
acfc961909 | ||
|
|
f502f75e1f | ||
|
|
ff97ef7b94 | ||
|
|
a2c780b085 | ||
|
|
b99305c909 | ||
|
|
d84dfc23e7 | ||
|
|
9d8fd3594e | ||
|
|
e68dbeb7eb | ||
|
|
32ddf0c296 | ||
|
|
c453bfeb32 | ||
|
|
f6ca450d36 | ||
|
|
d5f617ec92 | ||
|
|
6d104bfa91 | ||
|
|
e583cc2519 | ||
|
|
0d208b7957 | ||
|
|
43e5c042a2 | ||
|
|
39844ffef9 | ||
|
|
f5c8aac97d | ||
|
|
b6447ebdbb | ||
|
|
466fc4227e | ||
|
|
c034c88be4 | ||
|
|
72830efc45 | ||
|
|
c98eddc185 | ||
|
|
3b2353b5ae | ||
|
|
3f567c952c | ||
|
|
4f7f6a073c | ||
|
|
0e008cc15f | ||
|
|
1ad9c6faac | ||
|
|
06fe726ee7 | ||
|
|
1b6e46973e |
+11
-22
@@ -2,23 +2,12 @@ version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/golang:1.12
|
||||
- image: circleci/golang:1.13
|
||||
environment:
|
||||
GO111MODULE: "on"
|
||||
working_directory: /go/src/github.com/jesseduffield/lazygit
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Ensure go.mod file is up to date
|
||||
command: |
|
||||
export GO111MODULE=on
|
||||
rm go.sum
|
||||
mv go.mod /tmp/
|
||||
go mod init
|
||||
export GO111MODULE=auto
|
||||
|
||||
if [ $(diff /tmp/go.mod go.mod|wc -l) -gt 0 ]; then
|
||||
diff /tmp/go.mod go.mod
|
||||
exit 1;
|
||||
fi
|
||||
- run:
|
||||
name: Run gofmt -s
|
||||
command: |
|
||||
@@ -28,28 +17,28 @@ jobs:
|
||||
fi
|
||||
- restore_cache:
|
||||
keys:
|
||||
- pkg-cache-{{ checksum "Gopkg.lock" }}-v4
|
||||
- pkg-cache-{{ checksum "go.sum" }}-v5
|
||||
- run:
|
||||
name: Run tests
|
||||
command: |
|
||||
./test.sh
|
||||
- run:
|
||||
name: Compile project on every platform
|
||||
command: |
|
||||
go get github.com/mitchellh/gox
|
||||
gox -parallel 10 -os "linux freebsd netbsd windows" -osarch "darwin/i386 darwin/amd64"
|
||||
- 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 "Gopkg.lock" }}-v4
|
||||
key: pkg-cache-{{ checksum "go.sum" }}-v5
|
||||
paths:
|
||||
- ~/.cache/go-build
|
||||
|
||||
release:
|
||||
docker:
|
||||
- image: circleci/golang:1.10
|
||||
- image: circleci/golang:1.13
|
||||
working_directory: /go/src/github.com/jesseduffield/lazygit
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [jesseduffield]
|
||||
ko_fi: jesseduffield
|
||||
custom: ['https://donorbox.org/lazygit']
|
||||
@@ -1,6 +1,9 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
name: Discussion
|
||||
about: Begin a discussion
|
||||
title: ''
|
||||
labels: discussion
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Topic**
|
||||
A clear and concise description of what you want to discuss
|
||||
|
||||
**Your thoughts**
|
||||
What you have to say about the topic
|
||||
@@ -1,6 +1,9 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
+3
-3
@@ -2,12 +2,12 @@
|
||||
# docker build -t lazygit .
|
||||
# docker run -it lazygit:latest /bin/sh -l
|
||||
|
||||
FROM golang:alpine
|
||||
FROM golang:1.13-alpine3.10
|
||||
WORKDIR /go/src/github.com/jesseduffield/lazygit/
|
||||
COPY ./ .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o lazygit .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build
|
||||
|
||||
FROM alpine:latest
|
||||
FROM alpine:3.10
|
||||
RUN apk add -U git xdg-utils
|
||||
WORKDIR /go/src/github.com/jesseduffield/lazygit/
|
||||
COPY --from=0 /go/src/github.com/jesseduffield/lazygit /go/src/github.com/jesseduffield/lazygit
|
||||
|
||||
Generated
-652
@@ -1,652 +0,0 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
digest = "1:e24ea5dbc89fbab51635ee32e5be4f61a9267cae20788efcae4c07efb4abec99"
|
||||
name = "github.com/aws/aws-sdk-go"
|
||||
packages = [
|
||||
"aws",
|
||||
"aws/awserr",
|
||||
"aws/awsutil",
|
||||
"aws/client",
|
||||
"aws/client/metadata",
|
||||
"aws/corehandlers",
|
||||
"aws/credentials",
|
||||
"aws/credentials/ec2rolecreds",
|
||||
"aws/credentials/endpointcreds",
|
||||
"aws/credentials/stscreds",
|
||||
"aws/csm",
|
||||
"aws/defaults",
|
||||
"aws/ec2metadata",
|
||||
"aws/endpoints",
|
||||
"aws/request",
|
||||
"aws/session",
|
||||
"aws/signer/v4",
|
||||
"internal/sdkio",
|
||||
"internal/sdkrand",
|
||||
"internal/sdkuri",
|
||||
"internal/shareddefaults",
|
||||
"private/protocol",
|
||||
"private/protocol/eventstream",
|
||||
"private/protocol/eventstream/eventstreamapi",
|
||||
"private/protocol/query",
|
||||
"private/protocol/query/queryutil",
|
||||
"private/protocol/rest",
|
||||
"private/protocol/restxml",
|
||||
"private/protocol/xml/xmlutil",
|
||||
"service/s3",
|
||||
"service/sts",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "4324bc9d8865bdb3e6aa86ec7772ca1272d2750e"
|
||||
version = "v1.15.21"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:37011b20a70e205b93ebea5287e1afa5618db54bf3998c36ff5a8e4b146a170a"
|
||||
name = "github.com/bgentry/go-netrc"
|
||||
packages = ["netrc"]
|
||||
pruneopts = "NUT"
|
||||
revision = "9fd32a8b3d3d3f9d43c341bfe098430e07609480"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:cd7ba2b29e93e2a8384e813dfc80ebb0f85d9214762e6ca89bb55a58092eab87"
|
||||
name = "github.com/cloudfoundry/jibber_jabber"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "bcc4c8345a21301bf47c032ff42dd1aae2fe3027"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39"
|
||||
name = "github.com/davecgh/go-spew"
|
||||
packages = ["spew"]
|
||||
pruneopts = "NUT"
|
||||
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:de4a74b504df31145ffa8ca0c4edbffa2f3eb7f466753962184611b618fa5981"
|
||||
name = "github.com/emirpasic/gods"
|
||||
packages = [
|
||||
"containers",
|
||||
"lists",
|
||||
"lists/arraylist",
|
||||
"trees",
|
||||
"trees/binaryheap",
|
||||
"utils",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "f6c17b524822278a87e3b3bd809fec33b51f5b46"
|
||||
version = "v1.9.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:ade392a843b2035effb4b4a2efa2c3bab3eb29b992e98bacf9c898b0ecb54e45"
|
||||
name = "github.com/fatih/color"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4"
|
||||
version = "v1.7.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:1b91ae0dc69a41d4c2ed23ea5cffb721ea63f5037ca4b81e6d6771fbb8f45129"
|
||||
name = "github.com/fsnotify/fsnotify"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9"
|
||||
version = "v1.4.7"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:ea1d5bfdb4ec5c2ee48c97865e6de1a28fa8c4849a3f56b27d521aa619038e06"
|
||||
name = "github.com/go-errors/errors"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "a6af135bd4e28680facf08a3d206b454abc877a4"
|
||||
version = "v1.0.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:74d9b0a7b4107b41e0ade759fac64502876f82d29fb23d77b3dd24b194ee3dd5"
|
||||
name = "github.com/go-ini/ini"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "5cf292cae48347c2490ac1a58fe36735fb78df7e"
|
||||
version = "v1.38.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:4a8ed9b8cf22bd03bee5d74179fa06a282e4a73b6de949f7a865ff56cd2537e0"
|
||||
name = "github.com/golang-collections/collections"
|
||||
packages = ["stack"]
|
||||
pruneopts = "NUT"
|
||||
revision = "604e922904d35e97f98a774db7881f049cd8d970"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:a5d940c38bf56f121721bfa747c66356df387cb9d5318c570c6d4170aab62862"
|
||||
name = "github.com/hashicorp/go-cleanhttp"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "d5fe4b57a186c716b0e00b8c301cbd9b4182694d"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:b634d733abf079dc191d359e5a8d31479f1795d00e656f8a018a459571046266"
|
||||
name = "github.com/hashicorp/go-getter"
|
||||
packages = ["helper/url"]
|
||||
pruneopts = "NUT"
|
||||
revision = "4bda8fa99001c61db3cad96b421d4c12a81f256d"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:fbab03227343a0285fc74a68dd2ff46cda7edecbbe5a3e98d2cecd00cc67b217"
|
||||
name = "github.com/hashicorp/go-safetemp"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "b1a1dbde6fdc11e3ae79efd9039009e22d4ae240"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:0b06ffe0c0764e413a6738e3f045d6bb14117359aef80a09f8c60fbff2ecad6b"
|
||||
name = "github.com/hashicorp/go-version"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "b5a281d3160aa11950a6182bd9a9dc2cb1e02d50"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:11c6c696067d3127ecf332b10f89394d386d9083f82baf71f40f2da31841a009"
|
||||
name = "github.com/hashicorp/hcl"
|
||||
packages = [
|
||||
".",
|
||||
"hcl/ast",
|
||||
"hcl/parser",
|
||||
"hcl/printer",
|
||||
"hcl/scanner",
|
||||
"hcl/strconv",
|
||||
"hcl/token",
|
||||
"json/parser",
|
||||
"json/scanner",
|
||||
"json/token",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "ef8a98b0bbce4a65b5aa4c368430a80ddc533168"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:d457d39e88f678ed14ac29517c3d74927a48dbc6a9f073fa241cf364a68cbe5c"
|
||||
name = "github.com/heroku/rollrus"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "fc0cef2ff331aebb24cd4e9ded7e20650f3d7006"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:62fe3a7ea2050ecbd753a71889026f83d73329337ada66325cbafd5dea5f713d"
|
||||
name = "github.com/jbenet/go-context"
|
||||
packages = ["io"]
|
||||
pruneopts = "NUT"
|
||||
revision = "d14ea06fba99483203c19d92cfcd13ebe73135f4"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:490643e333b848f3d6ab772c21082d706663dcf4a3c0fbe9a4b4ef7b205ce6c7"
|
||||
name = "github.com/jesseduffield/go-getter"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "906e15686e6309ff310c1c10463ab53287c3a678"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:831819e8726b6b19e90079bce76d3048e2be3595359ee912d63d560e0db89b97"
|
||||
name = "github.com/jesseduffield/gocui"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "f0f0ab442e6c8e42650a7e38a45122bba2e11f3f"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:a46c2f4863e5284ddb255c28750298e04bc8c0fc896bed6056e947673168b7be"
|
||||
name = "github.com/jesseduffield/pty"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "02db52c7e406c7abec44c717a173c7715e4c1b62"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:3ab130f65766f5b7cc944d557df31c6a007ec017151705ec1e1b8719f2689021"
|
||||
name = "github.com/jesseduffield/termbox-go"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "1e272ff78dcb4c448870f464fda1cdcf2bf0b3dd"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:ac6d01547ec4f7f673311b4663909269bfb8249952de3279799289467837c3cc"
|
||||
name = "github.com/jmespath/go-jmespath"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "0b12d6b5"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:263f9b0a0bcbfff9d5e7d9f2aa11f53995d98214fe0fb97e429e7a5f4534a0f9"
|
||||
name = "github.com/kardianos/osext"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "ae77be60afb1dcacde03767a8c37337fad28ac14"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:8021af4dcbd531ae89433c8c3a6520e51064114aaf8eb1724c3cf911c497c9ba"
|
||||
name = "github.com/kevinburke/ssh_config"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "9fc7bb800b555d63157c65a904c86a2cc7b4e795"
|
||||
version = "0.4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:d244f8666a838fe6ad70ec8fe77f50ebc29fdc3331a2729ba5886bef8435d10d"
|
||||
name = "github.com/magiconair/properties"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "c2353362d570a7bfa228149c62842019201cfb71"
|
||||
version = "v1.8.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:08c231ec84231a7e23d67e4b58f975e1423695a32467a362ee55a803f9de8061"
|
||||
name = "github.com/mattn/go-colorable"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
|
||||
version = "v0.0.9"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:bc4f7eec3b7be8c6cb1f0af6c1e3333d5bb71072951aaaae2f05067b0803f287"
|
||||
name = "github.com/mattn/go-isatty"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
|
||||
version = "v0.0.3"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:cb591533458f6eb6e2c1065ff3eac6b50263d7847deb23fc9f79b25bc608970e"
|
||||
name = "github.com/mattn/go-runewidth"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "9e777a8366cce605130a531d2cd6363d07ad7317"
|
||||
version = "v0.0.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:a25c9a6b41e100f4ce164db80260f2b687095ba9d8b46a1d6072d3686cc020db"
|
||||
name = "github.com/mgutz/str"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "968bf66e3da857419e4f6e71b2d5c9ae95682dc4"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:a4df73029d2c42fabcb6b41e327d2f87e685284ec03edf76921c267d9cfc9c23"
|
||||
name = "github.com/mitchellh/go-homedir"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "58046073cbffe2f25d425fe1331102f55cf719de"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:18b773b92ac82a451c1276bd2776c1e55ce057ee202691ab33c8d6690efcc048"
|
||||
name = "github.com/mitchellh/go-testing-interface"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "a61a99592b77c9ba629d254a693acffaeb4b7e28"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:5fe20cfe4ef484c237cec9f947b2a6fa90bad4b8610fd014f0e4211e13d82d5d"
|
||||
name = "github.com/mitchellh/mapstructure"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "f15292f7a699fcc1a38a80977f80a046874ba8ac"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:2c34c77bf3ec848da26e48af58fc511ed52750961fa848399d122882b8890928"
|
||||
name = "github.com/nicksnyder/go-i18n"
|
||||
packages = [
|
||||
"v2/i18n",
|
||||
"v2/internal",
|
||||
"v2/internal/plural",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "a16b91a3ba80db3a2301c70d1d302d42251c9079"
|
||||
version = "v2.0.0-beta.5"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:cf254277d898b713195cc6b4a3fac8bf738b9f1121625df27843b52b267eec6c"
|
||||
name = "github.com/pelletier/go-buffruneio"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "c37440a7cf42ac63b919c752ca73a85067e05992"
|
||||
version = "v0.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:51ea800cff51752ff68e12e04106f5887b4daec6f9356721238c28019f0b42db"
|
||||
name = "github.com/pelletier/go-toml"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "c01d1270ff3e442a8a57cddc1c92dc1138598194"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:5cf3f025cbee5951a4ee961de067c8a89fc95a5adabead774f82822efabab121"
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe"
|
||||
name = "github.com/pmezard/go-difflib"
|
||||
packages = ["difflib"]
|
||||
pruneopts = "NUT"
|
||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:d917313f309bda80d27274d53985bc65651f81a5b66b820749ac7f8ef061fd04"
|
||||
name = "github.com/sergi/go-diff"
|
||||
packages = ["diffmatchpatch"]
|
||||
pruneopts = "NUT"
|
||||
revision = "1744e2970ca51c86172c8190fadad617561ed6e7"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:41618aee8828e62dfe62d44f579c06892d0e98907d1c6d5bcd83bfe8536ec5a3"
|
||||
name = "github.com/shibukawa/configdir"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "e180dbdc8da04c4fa04272e875ce64949f38bd3e"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:b2339e83ce9b5c4f79405f949429a7f68a9a904fed903c672aac1e7ceb7f5f02"
|
||||
name = "github.com/sirupsen/logrus"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "3e01752db0189b9157070a0e1668a620f9a85da2"
|
||||
version = "v1.0.6"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:330e9062b308ac597e28485699c02223bd052437a6eed32a173c9227dcb9d95a"
|
||||
name = "github.com/spf13/afero"
|
||||
packages = [
|
||||
".",
|
||||
"mem",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "787d034dfe70e44075ccc060d346146ef53270ad"
|
||||
version = "v1.1.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:3fa7947ca83b98ae553590d993886e845a4bff19b7b007e869c6e0dd3b9da9cd"
|
||||
name = "github.com/spf13/cast"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "8965335b8c7107321228e3e3702cab9832751bac"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:f29f83301ed096daed24a90f4af591b7560cb14b9cc3e1827abbf04db7269ab5"
|
||||
name = "github.com/spf13/jwalterweatherman"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "14d3d4c518341bea657dd8a226f5121c0ff8c9f2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:e3707aeaccd2adc89eba6c062fec72116fe1fc1ba71097da85b4d8ae1668a675"
|
||||
name = "github.com/spf13/pflag"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "9a97c102cda95a86cec2345a6f09f55a939babf5"
|
||||
version = "v1.0.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:454979540e2a1582f375a17c106cf4e11e3bcac4baffb4af23e515c87f87de13"
|
||||
name = "github.com/spf13/viper"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "907c19d40d9a6c9bb55f040ff4ae45271a4754b9"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:0e9a5ac14bcc11f205031a671b28c7e05cb88b2ebbe06f383c1ab0b2c12c7cb5"
|
||||
name = "github.com/spkg/bom"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "59b7046e48ad6bac800c5e1dd5142282cbfcf154"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:ccca1dcd18bc54e23b517a3c5babeff2e3924a7d8fc1932162225876cfe4bfb0"
|
||||
name = "github.com/src-d/gcfg"
|
||||
packages = [
|
||||
".",
|
||||
"scanner",
|
||||
"token",
|
||||
"types",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "f187355171c936ac84a82793659ebb4936bc1c23"
|
||||
version = "v1.3.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:bacb8b590716ab7c33f2277240972c9582d389593ee8d66fc10074e0508b8126"
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = ["assert"]
|
||||
pruneopts = "NUT"
|
||||
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
|
||||
version = "v1.2.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:e42372d3f4921ec35df07f9b23239631e9d28580f7c1edcca212bc6daddc68fe"
|
||||
name = "github.com/stvp/roll"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "3627a5cbeaeaa68023abd02bb8687925265f2f63"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:cd5ffc5bda4e0296ab3e4de90dbb415259c78e45e7fab13694b14cde8ab74541"
|
||||
name = "github.com/tcnksm/go-gitconfig"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "d154598bacbf4501c095a309753c5d4af66caa81"
|
||||
version = "v0.1.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:07e8742c479bab0066149ad02a710024154e76874fd0a2dba002d87702725825"
|
||||
name = "github.com/ulikunitz/xz"
|
||||
packages = [
|
||||
".",
|
||||
"internal/hash",
|
||||
"internal/xlog",
|
||||
"lzma",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "0c6b41e72360850ca4f98dc341fd999726ea007f"
|
||||
version = "v0.5.4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:3148cb3478c26a92b4c1a18abb9428234b281e278af6267840721a24b6cbc6a3"
|
||||
name = "github.com/xanzy/ssh-agent"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "640f0ab560aeb89d523bb6ac322b1244d5c3796c"
|
||||
version = "v0.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:dfcb1b2db354cafa48fc3cdafe4905a08bec4a9757919ab07155db0ca23855b4"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = [
|
||||
"cast5",
|
||||
"curve25519",
|
||||
"ed25519",
|
||||
"ed25519/internal/edwards25519",
|
||||
"internal/chacha20",
|
||||
"internal/subtle",
|
||||
"openpgp",
|
||||
"openpgp/armor",
|
||||
"openpgp/elgamal",
|
||||
"openpgp/errors",
|
||||
"openpgp/packet",
|
||||
"openpgp/s2k",
|
||||
"poly1305",
|
||||
"ssh",
|
||||
"ssh/agent",
|
||||
"ssh/knownhosts",
|
||||
"ssh/terminal",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "de0752318171da717af4ce24d0a2e8626afaeb11"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:76ee51c3f468493aff39dbacc401e8831fbb765104cbf613b89bef01cf4bad70"
|
||||
name = "golang.org/x/net"
|
||||
packages = ["context"]
|
||||
pruneopts = "NUT"
|
||||
revision = "c39426892332e1bb5ec0a434a079bf82f5d30c54"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:ec76a40fbfda0c329ee58f4e3b14b4279a939efce89eca020e934e2e5234eddd"
|
||||
name = "golang.org/x/sys"
|
||||
packages = [
|
||||
"unix",
|
||||
"windows",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "98c5dad5d1a0e8a73845ecc8897d0bd56586511d"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:a95288ef1ef4dfad6cba7fe30843e1683f71bc28c912ca1ba3f6a539d44db739"
|
||||
name = "golang.org/x/text"
|
||||
packages = [
|
||||
"internal/gen",
|
||||
"internal/tag",
|
||||
"internal/triegen",
|
||||
"internal/ucd",
|
||||
"language",
|
||||
"transform",
|
||||
"unicode/cldr",
|
||||
"unicode/norm",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:47a697b155f5214ff14e68e39ce9c2e8d93e1fb035ae5ba7e247d044e0ce64e3"
|
||||
name = "gopkg.in/src-d/go-billy.v4"
|
||||
packages = [
|
||||
".",
|
||||
"helper/chroot",
|
||||
"helper/polyfill",
|
||||
"osfs",
|
||||
"util",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "83cf655d40b15b427014d7875d10850f96edba14"
|
||||
version = "v4.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:e66078da2bd6e53c72518d7f6ae0c3c8c7f34c0df12c39435ce34a6bce165525"
|
||||
name = "gopkg.in/src-d/go-git.v4"
|
||||
packages = [
|
||||
".",
|
||||
"config",
|
||||
"internal/revision",
|
||||
"plumbing",
|
||||
"plumbing/cache",
|
||||
"plumbing/filemode",
|
||||
"plumbing/format/config",
|
||||
"plumbing/format/diff",
|
||||
"plumbing/format/gitignore",
|
||||
"plumbing/format/idxfile",
|
||||
"plumbing/format/index",
|
||||
"plumbing/format/objfile",
|
||||
"plumbing/format/packfile",
|
||||
"plumbing/format/pktline",
|
||||
"plumbing/object",
|
||||
"plumbing/protocol/packp",
|
||||
"plumbing/protocol/packp/capability",
|
||||
"plumbing/protocol/packp/sideband",
|
||||
"plumbing/revlist",
|
||||
"plumbing/storer",
|
||||
"plumbing/transport",
|
||||
"plumbing/transport/client",
|
||||
"plumbing/transport/file",
|
||||
"plumbing/transport/git",
|
||||
"plumbing/transport/http",
|
||||
"plumbing/transport/internal/common",
|
||||
"plumbing/transport/server",
|
||||
"plumbing/transport/ssh",
|
||||
"storage",
|
||||
"storage/filesystem",
|
||||
"storage/filesystem/dotgit",
|
||||
"storage/memory",
|
||||
"utils/binary",
|
||||
"utils/diff",
|
||||
"utils/ioutil",
|
||||
"utils/merkletrie",
|
||||
"utils/merkletrie/filesystem",
|
||||
"utils/merkletrie/index",
|
||||
"utils/merkletrie/internal/frame",
|
||||
"utils/merkletrie/noder",
|
||||
]
|
||||
pruneopts = "NUT"
|
||||
revision = "43d17e14b714665ab5bc2ecc220b6740779d733f"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:b233ad4ec87ac916e7bf5e678e98a2cb9e8b52f6de6ad3e11834fc7a71b8e3bf"
|
||||
name = "gopkg.in/warnings.v0"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "ec4a0fea49c7b46c2aeb0b51aac55779c607e52b"
|
||||
version = "v0.1.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:7c95b35057a0ff2e19f707173cc1a947fa43a6eb5c4d300d196ece0334046082"
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
pruneopts = "NUT"
|
||||
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
|
||||
version = "v2.2.1"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
input-imports = [
|
||||
"github.com/cloudfoundry/jibber_jabber",
|
||||
"github.com/fatih/color",
|
||||
"github.com/go-errors/errors",
|
||||
"github.com/golang-collections/collections/stack",
|
||||
"github.com/heroku/rollrus",
|
||||
"github.com/jesseduffield/go-getter",
|
||||
"github.com/jesseduffield/gocui",
|
||||
"github.com/jesseduffield/pty",
|
||||
"github.com/kardianos/osext",
|
||||
"github.com/mgutz/str",
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n",
|
||||
"github.com/shibukawa/configdir",
|
||||
"github.com/sirupsen/logrus",
|
||||
"github.com/spf13/viper",
|
||||
"github.com/spkg/bom",
|
||||
"github.com/stretchr/testify/assert",
|
||||
"github.com/tcnksm/go-gitconfig",
|
||||
"golang.org/x/text/language",
|
||||
"gopkg.in/src-d/go-git.v4",
|
||||
"gopkg.in/src-d/go-git.v4/plumbing",
|
||||
"gopkg.in/yaml.v2",
|
||||
]
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
# Gopkg.toml example
|
||||
#
|
||||
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
|
||||
# for detailed Gopkg.toml documentation.
|
||||
#
|
||||
# required = ["github.com/user/thing/cmd/thing"]
|
||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project"
|
||||
# version = "1.0.0"
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project2"
|
||||
# branch = "dev"
|
||||
# source = "github.com/myfork/project2"
|
||||
#
|
||||
# [[override]]
|
||||
# name = "github.com/x/y"
|
||||
# version = "2.4.0"
|
||||
#
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
||||
non-go = true
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/fatih/color"
|
||||
version = "1.7.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/golang-collections/collections"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/jesseduffield/gocui"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/jesseduffield/pty"
|
||||
|
||||
[[constraint]]
|
||||
name = "gopkg.in/src-d/go-git.v4"
|
||||
revision = "43d17e14b714665ab5bc2ecc220b6740779d733f"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/spkg/bom"
|
||||
@@ -2,26 +2,38 @@
|
||||
|
||||
A simple terminal UI for git commands, written in Go with the [gocui](https://github.com/jroimartin/gocui 'gocui') library.
|
||||
|
||||
Are YOU tired of typing every git command directly into the terminal, but you're
|
||||
too stubborn to use Sourcetree because you'll never forgive Atlassian for making
|
||||
Jira? This is the app for you!
|
||||
Rant time: You've heard it before, git is _powerful_, but what good is that power when everything is so damn hard to do? Interactive rebasing requires you to edit a goddamn TODO file in your editor? *Are you kidding me?* To stage part of a file you need to use a command line program stepping through each hunk and if a hunk can't be split down any further but contains code you don't want to stage, bad luck? *Are you KIDDING me?!* Sometimes you get asked to stash your changes when switching branches only to realise that after you switch and unstash that there weren't even any conflicts and it would have been fine to just checkout the branch directly? *YOU HAVE GOT TO BE KIDDING ME!*
|
||||
|
||||
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](https://github.com/jesseduffield/lazygit/blob/master/docs/Keybindings_en.md)
|
||||
[Keybindings](/docs/keybindings)
|
||||
- [Cool Features](https://github.com/jesseduffield/lazygit#cool-features)
|
||||
- [Contributing](https://github.com/jesseduffield/lazygit#contributing)
|
||||
- [Video Tutorial](https://youtu.be/VDXvbHZYeKY)
|
||||
- [Rebase Magic Video Tutorial](https://youtu.be/4XaToVut_hs)
|
||||
- [Twitch Stream](https://www.twitch.tv/jesseduffield)
|
||||
|
||||
[<img src="https://i.imgur.com/sVEktDn.png">](https://youtu.be/CPLdltN7wgE)
|
||||
|
||||
Github Sponsors is matching all donations dollar-for-dollar for 12 months so if you're feeling generous consider [sponsoring me](https://github.com/sponsors/jesseduffield)
|
||||
|
||||
## Installation
|
||||
|
||||
### 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.
|
||||
|
||||
```sh
|
||||
brew tap jesseduffield/lazygit
|
||||
Tap:
|
||||
```
|
||||
brew install jesseduffield/lazygit/lazygit
|
||||
```
|
||||
|
||||
Core:
|
||||
|
||||
```
|
||||
brew install lazygit
|
||||
```
|
||||
|
||||
@@ -103,8 +115,27 @@ 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_en.md).
|
||||
[here](/docs/keybindings).
|
||||
|
||||
## Changing Directory On Exit
|
||||
|
||||
If you change repos in lazygit and want your shell to change directory into that repo on exiting lazygit, add this to your `~./zshrc` (or other rc file):
|
||||
```
|
||||
lg()
|
||||
{
|
||||
export LAZYGIT_NEW_DIR_FILE=~/.lazygit/newdir
|
||||
|
||||
lazygit "$@"
|
||||
|
||||
if [ -f $LAZYGIT_NEW_DIR_FILE ]; then
|
||||
cd "$(cat $LAZYGIT_NEW_DIR_FILE)"
|
||||
rm -f $LAZYGIT_NEW_DIR_FILE > /dev/null
|
||||
fi
|
||||
}
|
||||
```
|
||||
Then `source ~/.zshrc` and from now on when you call `lg` and exit you'll switch directories to whatever you were in inside lazyigt. To override this behaviour you can exit using `shift+Q` rather than just `q`.
|
||||
|
||||
## Cool features
|
||||
|
||||
@@ -132,9 +163,7 @@ For contributor discussion about things not better discussed here in the repo, j
|
||||
|
||||
## Donate
|
||||
|
||||
If you would like to support the development of lazygit, please donate
|
||||
|
||||
[](https://donorbox.org/lazygit)
|
||||
If you would like to support the development of lazygit, consider [sponsoring me](https://github.com/sponsors/jesseduffield) (github is matching all donations dollar-for-dollar for 12 months)
|
||||
|
||||
## Work in progress
|
||||
|
||||
|
||||
+24
-5
@@ -2,12 +2,13 @@
|
||||
|
||||
## Default:
|
||||
|
||||
```
|
||||
```yaml
|
||||
gui:
|
||||
# stuff relating to the UI
|
||||
scrollHeight: 2 # how many lines you scroll by
|
||||
scrollPastBottom: true # enable scrolling past the bottom
|
||||
theme:
|
||||
lightTheme: false # For terminals with a light background
|
||||
activeBorderColor:
|
||||
- white
|
||||
- bold
|
||||
@@ -17,10 +18,13 @@
|
||||
- blue
|
||||
commitLength:
|
||||
show: true
|
||||
mouseEvents: true
|
||||
git:
|
||||
merging:
|
||||
# only applicable to unix users
|
||||
manualCommit: false
|
||||
skipHookPrefix: WIP
|
||||
autoFetch: true
|
||||
update:
|
||||
method: prompt # can be: prompt | background | never
|
||||
days: 14 # how often an update is checked for
|
||||
@@ -32,21 +36,21 @@
|
||||
|
||||
### Windows:
|
||||
|
||||
```
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'cmd /c "start "" {{filename}}"'
|
||||
```
|
||||
|
||||
### Linux:
|
||||
|
||||
```
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'
|
||||
```
|
||||
|
||||
### OSX:
|
||||
|
||||
```
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'open {{filename}}'
|
||||
```
|
||||
@@ -55,7 +59,7 @@
|
||||
|
||||
for users of VSCode
|
||||
|
||||
```
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'code -r {{filename}}'
|
||||
```
|
||||
@@ -78,6 +82,21 @@ The available attributes are:
|
||||
- reverse # useful for high-contrast
|
||||
- underline
|
||||
|
||||
## Light terminal theme:
|
||||
|
||||
If you have issues with a light terminal theme where you can't read / see the text add these settings
|
||||
|
||||
```yaml
|
||||
gui:
|
||||
theme:
|
||||
lightTheme: true
|
||||
activeBorderColor:
|
||||
- black
|
||||
- bold
|
||||
inactiveBorderColor:
|
||||
- black
|
||||
```
|
||||
|
||||
## Example Coloring:
|
||||
|
||||

|
||||
|
||||
@@ -22,21 +22,22 @@
|
||||
|
||||
<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>: delete if untracked / checkout if tracked
|
||||
<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>s</kbd>: soft reset to last commit
|
||||
<kbd>a</kbd>: stage/unstage all
|
||||
<kbd>t</kbd>: add patch
|
||||
<kbd>D</kbd>: reset hard and remove untracked files
|
||||
<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
|
||||
@@ -61,9 +62,11 @@
|
||||
<kbd>R</kbd>: rename commit with editor
|
||||
<kbd>g</kbd>: reset to this commit
|
||||
<kbd>f</kbd>: fixup commit
|
||||
<kbd>F</kbd>: create fixup commit for this commit
|
||||
<kbd>S</kbd>: squash above commits
|
||||
<kbd>d</kbd>: delete commit
|
||||
<kbd>J</kbd>: move commit down one
|
||||
<kbd>K</kbd>: move commit up one
|
||||
<kbd>ctrl+j</kbd>: move commit down one
|
||||
<kbd>ctrl+k</kbd>: move commit up one
|
||||
<kbd>e</kbd>: edit commit
|
||||
<kbd>A</kbd>: amend commit with staged changes
|
||||
<kbd>p</kbd>: pick commit (when mid-rebase)
|
||||
@@ -71,6 +74,8 @@
|
||||
<kbd>c</kbd>: copy commit (cherry-pick)
|
||||
<kbd>C</kbd>: copy commit range (cherry-pick)
|
||||
<kbd>v</kbd>: paste commits (cherry-pick)
|
||||
<kbd>enter</kbd>: view commit's files
|
||||
<kbd>space</kbd>: select commit to diff with another commit
|
||||
</pre>
|
||||
|
||||
## Stash
|
||||
@@ -81,11 +86,20 @@
|
||||
<kbd>d</kbd>: drop
|
||||
</pre>
|
||||
|
||||
## Commit files
|
||||
|
||||
<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
|
||||
</pre>
|
||||
|
||||
## Main (Normal)
|
||||
|
||||
<pre>
|
||||
<kbd>PgDn</kbd>: scroll down
|
||||
<kbd>PgUp</kbd>: scroll up
|
||||
<kbd>PgDn</kbd>: scroll down (fn+up)
|
||||
<kbd>PgUp</kbd>: scroll up (fn+down)
|
||||
</pre>
|
||||
|
||||
## Main (Staging)
|
||||
@@ -0,0 +1,128 @@
|
||||
# Lazygit menu
|
||||
|
||||
## Global
|
||||
|
||||
<pre>
|
||||
<kbd>m</kbd>: bekijk merge/rebase opties
|
||||
<kbd>P</kbd>: push
|
||||
<kbd>p</kbd>: pull
|
||||
<kbd>R</kbd>: verversen
|
||||
</pre>
|
||||
|
||||
## Status
|
||||
|
||||
<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
|
||||
</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
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: uitchecken
|
||||
<kbd>o</kbd>: maak een pull-aanvraag
|
||||
<kbd>c</kbd>: uitchecken bij naam
|
||||
<kbd>F</kbd>: forceer checkout
|
||||
<kbd>n</kbd>: nieuwe branch
|
||||
<kbd>d</kbd>: verwijder branch
|
||||
<kbd>r</kbd>: rebase branch
|
||||
<kbd>M</kbd>: merge in met huidige checked out branch
|
||||
<kbd>f</kbd>: fast-forward this branch from its upstream
|
||||
</pre>
|
||||
|
||||
## Commits
|
||||
|
||||
<pre>
|
||||
<kbd>s</kbd>: squash beneden
|
||||
<kbd>r</kbd>: hernoem commit
|
||||
<kbd>R</kbd>: rename commit with editor
|
||||
<kbd>g</kbd>: reset naar deze commit
|
||||
<kbd>f</kbd>: Fixup commit
|
||||
<kbd>F</kbd>: creëer fixup commit voor deze commit
|
||||
<kbd>S</kbd>: squash bovenstaande commits
|
||||
<kbd>d</kbd>: verwijder commit
|
||||
<kbd>ctrl+j</kbd>: verplaats commit 1 omlaag
|
||||
<kbd>ctrl+k</kbd>: verplaats commit 1 omhoog
|
||||
<kbd>e</kbd>: verander commit
|
||||
<kbd>A</kbd>: wijzig commit met staged veranderingen
|
||||
<kbd>p</kbd>: pick commit (when mid-rebase)
|
||||
<kbd>t</kbd>: commit omgedaan maken
|
||||
<kbd>c</kbd>: kopiëer commit (cherry-pick)
|
||||
<kbd>C</kbd>: kopiëer commit reeks (cherry-pick)
|
||||
<kbd>v</kbd>: plak commits (cherry-pick)
|
||||
<kbd>enter</kbd>: bekijk gecommite bestanden
|
||||
<kbd>space</kbd>: select commit to diff with another commit
|
||||
</pre>
|
||||
|
||||
## Stash
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: toepassen
|
||||
<kbd>g</kbd>: pop
|
||||
<kbd>d</kbd>: drop
|
||||
</pre>
|
||||
|
||||
## Commit bestanden
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: ga terug
|
||||
<kbd>c</kbd>: bestand uitchecken
|
||||
<kbd>d</kbd>: uitsluit deze commit zijn veranderingen aan dit bestand
|
||||
<kbd>o</kbd>: open bestand
|
||||
</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)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: ga terug naar het bestanden paneel
|
||||
<kbd>space</kbd>: pick hunk
|
||||
<kbd>b</kbd>: pick beide hunks
|
||||
<kbd>◄</kbd>: selecteer voorgaand conflict
|
||||
<kbd>►</kbd>: selecteer volgende conflict
|
||||
<kbd>▲</kbd>: selecteer bovenste hunk
|
||||
<kbd>▼</kbd>: selecteer onderste hunk
|
||||
<kbd>z</kbd>: ongedaan maken
|
||||
</pre>
|
||||
|
||||
## Hoofd (Normaal)
|
||||
|
||||
<pre>
|
||||
<kbd>PgDn</kbd>: scroll omlaag (fn+up)
|
||||
<kbd>PgUp</kbd>: scroll omhoog (fn+down)
|
||||
</pre>
|
||||
@@ -0,0 +1,128 @@
|
||||
# Lazygit menu
|
||||
|
||||
## Globalne
|
||||
|
||||
<pre>
|
||||
<kbd>m</kbd>: view merge/rebase options
|
||||
<kbd>P</kbd>: push
|
||||
<kbd>p</kbd>: pull
|
||||
<kbd>R</kbd>: odśwież
|
||||
</pre>
|
||||
|
||||
## Status
|
||||
|
||||
<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
|
||||
</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
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: przełącz
|
||||
<kbd>o</kbd>: utwórz żądanie wyciągnięcia
|
||||
<kbd>c</kbd>: przełącz używając nazwy
|
||||
<kbd>F</kbd>: wymuś przełączenie
|
||||
<kbd>n</kbd>: nowa gałąź
|
||||
<kbd>d</kbd>: usuń gałąź
|
||||
<kbd>r</kbd>: rebase branch
|
||||
<kbd>M</kbd>: scal do obecnej gałęzi
|
||||
<kbd>f</kbd>: fast-forward this branch from its upstream
|
||||
</pre>
|
||||
|
||||
## Commity
|
||||
|
||||
<pre>
|
||||
<kbd>s</kbd>: ściśnij w dół
|
||||
<kbd>r</kbd>: przemianuj commit
|
||||
<kbd>R</kbd>: przemianuj commit w edytorze
|
||||
<kbd>g</kbd>: zresetuj do tego commita
|
||||
<kbd>f</kbd>: napraw commit
|
||||
<kbd>F</kbd>: create fixup commit for this commit
|
||||
<kbd>S</kbd>: squash above commits
|
||||
<kbd>d</kbd>: delete commit
|
||||
<kbd>ctrl+j</kbd>: move commit down one
|
||||
<kbd>ctrl+k</kbd>: move commit up one
|
||||
<kbd>e</kbd>: edit commit
|
||||
<kbd>A</kbd>: amend commit with staged changes
|
||||
<kbd>p</kbd>: pick commit (when mid-rebase)
|
||||
<kbd>t</kbd>: revert commit
|
||||
<kbd>c</kbd>: copy commit (cherry-pick)
|
||||
<kbd>C</kbd>: copy commit range (cherry-pick)
|
||||
<kbd>v</kbd>: paste commits (cherry-pick)
|
||||
<kbd>enter</kbd>: view commit's files
|
||||
<kbd>space</kbd>: select commit to diff with another commit
|
||||
</pre>
|
||||
|
||||
## Schowek
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: zastosuj
|
||||
<kbd>g</kbd>: wyciągnij
|
||||
<kbd>d</kbd>: porzuć
|
||||
</pre>
|
||||
|
||||
## Commit files
|
||||
|
||||
<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
|
||||
</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)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: wróć do panelu plików
|
||||
<kbd>space</kbd>: pick hunk
|
||||
<kbd>b</kbd>: pick both hunks
|
||||
<kbd>◄</kbd>: select previous conflict
|
||||
<kbd>►</kbd>: select next conflict
|
||||
<kbd>▲</kbd>: select top hunk
|
||||
<kbd>▼</kbd>: select bottom hunk
|
||||
<kbd>z</kbd>: undo
|
||||
</pre>
|
||||
@@ -1,66 +1,34 @@
|
||||
module github.com/jesseduffield/lazygit
|
||||
|
||||
go 1.12
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go v1.15.21
|
||||
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d
|
||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
|
||||
github.com/davecgh/go-spew v1.1.0
|
||||
github.com/emirpasic/gods v1.9.0
|
||||
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-ini/ini v1.38.2
|
||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
|
||||
github.com/hashicorp/go-cleanhttp v0.0.0-20171218145408-d5fe4b57a186
|
||||
github.com/hashicorp/go-getter v0.0.0-20180809191950-4bda8fa99001
|
||||
github.com/hashicorp/go-safetemp v0.0.0-20180326211150-b1a1dbde6fdc
|
||||
github.com/hashicorp/go-version v1.0.0
|
||||
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce
|
||||
github.com/heroku/rollrus v0.0.0-20180515183152-fc0cef2ff331
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99
|
||||
github.com/jesseduffield/go-getter v0.0.0-20180822080847-906e15686e63
|
||||
github.com/jesseduffield/gocui v0.0.0-20190305102456-f0f0ab442e6c
|
||||
github.com/jesseduffield/pty v0.0.0-20181218102224-02db52c7e406
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20180919093808-1e272ff78dcb
|
||||
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8
|
||||
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1
|
||||
github.com/kevinburke/ssh_config v0.0.0-20180317175531-9fc7bb800b55
|
||||
github.com/magiconair/properties v1.8.0
|
||||
github.com/mattn/go-colorable v0.0.9
|
||||
github.com/mattn/go-isatty v0.0.3
|
||||
github.com/mattn/go-runewidth v0.0.2
|
||||
github.com/golang/protobuf v1.3.2 // indirect
|
||||
github.com/google/go-cmp v0.3.1 // indirect
|
||||
github.com/integrii/flaggy v1.3.0
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20191110053728-01cdcccd0508
|
||||
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/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
|
||||
github.com/mattn/go-colorable v0.1.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.10 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.6 // indirect
|
||||
github.com/mgutz/str v1.2.0
|
||||
github.com/mitchellh/go-homedir v0.0.0-20180801233206-58046073cbff
|
||||
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77
|
||||
github.com/mitchellh/mapstructure v0.0.0-20180715050151-f15292f7a699
|
||||
github.com/nicksnyder/go-i18n v0.0.0-20180803040939-a16b91a3ba80
|
||||
github.com/pelletier/go-buffruneio v0.2.0
|
||||
github.com/pelletier/go-toml v1.2.0
|
||||
github.com/pkg/errors v0.8.0
|
||||
github.com/pmezard/go-difflib v1.0.0
|
||||
github.com/sergi/go-diff v1.0.0
|
||||
github.com/nicksnyder/go-i18n/v2 v2.0.2
|
||||
github.com/onsi/ginkgo v1.10.3 // indirect
|
||||
github.com/onsi/gomega v1.7.1 // indirect
|
||||
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0
|
||||
github.com/sirupsen/logrus v1.0.6
|
||||
github.com/spf13/afero v1.1.1
|
||||
github.com/spf13/cast v1.2.0
|
||||
github.com/spf13/jwalterweatherman v0.0.0-20180814060501-14d3d4c51834
|
||||
github.com/spf13/pflag v1.0.2
|
||||
github.com/spf13/viper v1.1.0
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/spf13/viper v1.5.0
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
|
||||
github.com/src-d/gcfg v1.3.0
|
||||
github.com/stretchr/testify v1.2.2
|
||||
github.com/stvp/roll v0.0.0-20170522205222-3627a5cbeaea
|
||||
github.com/stretchr/testify v1.4.0
|
||||
github.com/tcnksm/go-gitconfig v0.1.2
|
||||
github.com/ulikunitz/xz v0.5.4
|
||||
github.com/xanzy/ssh-agent v0.2.0
|
||||
golang.org/x/crypto v0.0.0-20180808211826-de0752318171
|
||||
golang.org/x/net v0.0.0-20180811021610-c39426892332
|
||||
golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0
|
||||
golang.org/x/text v0.3.0
|
||||
gopkg.in/src-d/go-billy.v4 v4.2.0
|
||||
gopkg.in/src-d/go-git.v4 v4.0.0-20180807092216-43d17e14b714
|
||||
gopkg.in/warnings.v0 v0.1.2
|
||||
gopkg.in/yaml.v2 v2.2.1
|
||||
golang.org/x/text v0.3.2
|
||||
gopkg.in/src-d/go-git.v4 v4.13.1
|
||||
gopkg.in/yaml.v2 v2.2.5
|
||||
)
|
||||
|
||||
@@ -1,123 +1,272 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/aws/aws-sdk-go v1.15.21 h1:STLvc6RrpycslC1NRtTvt/YSgDkIGCTrB9K9vE5R2oQ=
|
||||
github.com/aws/aws-sdk-go v1.15.21/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
|
||||
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=
|
||||
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
|
||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do=
|
||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emirpasic/gods v1.9.0 h1:rUF4PuzEjMChMiNsVjdI+SyLu7rEqpQ5reNFnhC7oFo=
|
||||
github.com/emirpasic/gods v1.9.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
|
||||
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
|
||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-ini/ini v1.38.2 h1:6Hl/z3p3iFkA0dlDfzYxuFuUGD+kaweypF6btsR2/Q4=
|
||||
github.com/go-ini/ini v1.38.2/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 h1:zN2lZNZRflqFyxVaTIU61KNKQ9C0055u9CAfpmqUvo4=
|
||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3/go.mod h1:nPpo7qLxd6XL3hWJG/O60sR8ZKfMCiIoNap5GvD12KU=
|
||||
github.com/hashicorp/go-cleanhttp v0.0.0-20171218145408-d5fe4b57a186 h1:URgjUo+bs1KwatoNbwG0uCO4dHN4r1jsp4a5AGgHRjo=
|
||||
github.com/hashicorp/go-cleanhttp v0.0.0-20171218145408-d5fe4b57a186/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-getter v0.0.0-20180809191950-4bda8fa99001 h1:MFPzqpPED05pFyGjNPJEC2sXM6EHTzFyvX+0s0JoZ48=
|
||||
github.com/hashicorp/go-getter v0.0.0-20180809191950-4bda8fa99001/go.mod h1:6rdJFnhkXnzGOJbvkrdv4t9nLwKcVA+tmbQeUlkIzrU=
|
||||
github.com/hashicorp/go-safetemp v0.0.0-20180326211150-b1a1dbde6fdc h1:wAa9fGALVHfjYxZuXRnmuJG2CnwRpJYOTvY6YdErAh0=
|
||||
github.com/hashicorp/go-safetemp v0.0.0-20180326211150-b1a1dbde6fdc/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I=
|
||||
github.com/hashicorp/go-version v1.0.0 h1:21MVWPKDphxa7ineQQTrCU5brh7OuVVAzGOCnnCPtE8=
|
||||
github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce h1:xdsDDbiBDQTKASoGEZ+pEmF1OnWuu8AQ9I8iNbHNeno=
|
||||
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
|
||||
github.com/heroku/rollrus v0.0.0-20180515183152-fc0cef2ff331 h1:qio0y/sQdhbHRA3cmgczo04MaSV2zw+n46G1owvgWIk=
|
||||
github.com/heroku/rollrus v0.0.0-20180515183152-fc0cef2ff331/go.mod h1:BT+PgT529opmb6mcUY+Fg0IwVRRmwqFyavEMU17GnBg=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1 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=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/integrii/flaggy v1.3.0 h1:8I5Qqz22C6+EwUqJuaN5ITh77obI8VSg6RwYLe2VB7o=
|
||||
github.com/integrii/flaggy v1.3.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/go-getter v0.0.0-20180822080847-906e15686e63 h1:Nrr/yUxNjXWYK0B3IqcFlYh1ICnesJDB4ogcfOVc5Ns=
|
||||
github.com/jesseduffield/go-getter v0.0.0-20180822080847-906e15686e63/go.mod h1:fNqjRf+4XnTo2PrGN1JRb79b/BeoHwP4lU00f39SQY0=
|
||||
github.com/jesseduffield/gocui v0.0.0-20190305102456-f0f0ab442e6c h1:ZmCZFQukfddcISnY8R5YYMCYJX0mc1Wfmq/MhEQViUE=
|
||||
github.com/jesseduffield/gocui v0.0.0-20190305102456-f0f0ab442e6c/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/pty v0.0.0-20181218102224-02db52c7e406 h1:iYMH6h6SuWuBkIzRtymosE8NpSgTK0oRMfyTdVWgxzc=
|
||||
github.com/jesseduffield/pty v0.0.0-20181218102224-02db52c7e406/go.mod h1:7jlS40+UhOqkZJDIG1B/H21xnuET/+fvbbnHCa8wSIo=
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20180919093808-1e272ff78dcb h1:cFHYEWpQEfzFZVKiKZytCUX4UwQixKSw0kd3WhluPsY=
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20180919093808-1e272ff78dcb/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 h1:12VvqtR6Aowv3l/EQUlocDHW2Cp4G9WJVH7uyH8QFJE=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 h1:PJPDf8OUfOK1bb/NeTKd4f1QXZItOX389VN3B6qC8ro=
|
||||
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
|
||||
github.com/kevinburke/ssh_config v0.0.0-20180317175531-9fc7bb800b55 h1:S38dC4mEwxdw/U41+97VWdbun8mTcTjwg5Ujfg8QPME=
|
||||
github.com/kevinburke/ssh_config v0.0.0-20180317175531-9fc7bb800b55/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o=
|
||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20191110053728-01cdcccd0508 h1:8CPQLUe+0QXxnbnUfaMxh1UGxg3rYCqCvbxecC9rrIY=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20191110053728-01cdcccd0508/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/pty v1.1.3 h1:JTPQ/bbGH3IBwQhxKZiFxfD4wv7cYrY7LNTwbucsZ64=
|
||||
github.com/jesseduffield/pty v1.1.3/go.mod h1:7jlS40+UhOqkZJDIG1B/H21xnuET/+fvbbnHCa8wSIo=
|
||||
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/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/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
|
||||
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
|
||||
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10=
|
||||
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||
github.com/mattn/go-runewidth v0.0.6 h1:V2iyH+aX9C5fsYCpK60U8BYIvmhqxuOL3JZcqc1NB7k=
|
||||
github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mgutz/str v1.2.0 h1:4IzWSdIz9qPQWLfKZ0rJcV0jcUDpxvP4JVZ4GXQyvSw=
|
||||
github.com/mgutz/str v1.2.0/go.mod h1:w1v0ofgLaJdoD0HpQ3fycxKD1WtxpjSo151pK/31q6w=
|
||||
github.com/mitchellh/go-homedir v0.0.0-20180801233206-58046073cbff h1:jM4Eo4qMmmcqePS3u6X2lcEELtVuXWkWJIS/pRI3oSk=
|
||||
github.com/mitchellh/go-homedir v0.0.0-20180801233206-58046073cbff/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 h1:7GoSOOW2jpsfkntVKaS2rAr1TJqfcxotyaUcuxoZSzg=
|
||||
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20180715050151-f15292f7a699 h1:KXZJFdun9knAVAR8tg/aHJEr5DgtcbqyvzacK+CDCaI=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20180715050151-f15292f7a699/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/nicksnyder/go-i18n v0.0.0-20180803040939-a16b91a3ba80 h1:7ory6RlsEkeK89iyV7Imz3sVz8YHeSw29w3PehpCWC0=
|
||||
github.com/nicksnyder/go-i18n v0.0.0-20180803040939-a16b91a3ba80/go.mod h1:e4Di5xjP9oTVrC6y3C7C0HoSYXjSbhh/dU0eUV32nB4=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.0.0-beta.6 h1:SooTCzUOOs899x/M7gmSS+dgL+AUfSWqAcHLN3auCic=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.0.0-beta.6/go.mod h1:4Opqa6/HIv0lhG3WRAkqzO0afezkRhxXI0P8EJkqeRU=
|
||||
github.com/pelletier/go-buffruneio v0.2.0 h1:U4t4R6YkofJ5xHm3dJzuRpPZ0mr5MMCoAWooScCR7aA=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.0.2 h1:KsHGcTByIM0mHZKQGy0nlJLOjPNjQ6MVib/3PvsBDNY=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.0.2/go.mod h1:JXS4+OKhbcwDoVTEj0sLFWL1vOwec2g/YBAxZ9owJqY=
|
||||
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/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 h1:Xuk8ma/ibJ1fOy4Ee11vHhUFHQNpHhrBneOCNHVXS5w=
|
||||
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0/go.mod h1:7AwjWCpdPhkSmNAgUv5C7EJ4AbmjEB3r047r3DXWu3Y=
|
||||
github.com/sirupsen/logrus v1.0.6 h1:hcP1GmhGigz/O7h1WVUM5KklBp1JoNS9FggWKdj/j3s=
|
||||
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
github.com/spf13/afero v1.1.1 h1:Lt3ihYMlE+lreX1GS4Qw4ZsNpYQLxIXKBTEOXm3nt6I=
|
||||
github.com/spf13/afero v1.1.1/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.2.0 h1:HHl1DSRbEQN2i8tJmtS6ViPyHx35+p51amrdsiTCrkg=
|
||||
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
|
||||
github.com/spf13/jwalterweatherman v0.0.0-20180814060501-14d3d4c51834 h1:kJI9pPzfsULT/72wy7mxkRQZPtKWgFdCA2RTGZ4v8/E=
|
||||
github.com/spf13/jwalterweatherman v0.0.0-20180814060501-14d3d4c51834/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.2 h1:Fy0orTDgHdbnzHcsOgfCN4LtHf0ec3wwtiwJqwvf3Gc=
|
||||
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.1.0 h1:V7OZpY8i3C1x/pDmU0zNNlfVoDz112fSYvtWMjjS3f4=
|
||||
github.com/spf13/viper v1.1.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
|
||||
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/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/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/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.5.0 h1:GpsTwfsQ27oS/Aha/6d1oD7tpKIqWnOA6tgOX9HHkt4=
|
||||
github.com/spf13/viper v1.5.0/go.mod h1:AkYRkVJF8TkSG/xet6PzXX+l39KhhXa2pdqVSxnTcn4=
|
||||
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.3.0 h1:2BEDr8r0I0b8h/fOqwtxCEiq2HJu8n2JGZJQFGXWLjg=
|
||||
github.com/src-d/gcfg v1.3.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
|
||||
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/stvp/roll v0.0.0-20170522205222-3627a5cbeaea h1:jysxIKov/4GJ33wI2aRvuIK7yBwB28E5almlgDLPRpM=
|
||||
github.com/stvp/roll v0.0.0-20170522205222-3627a5cbeaea/go.mod h1:Ffmqrj3nXIMIjeA4uW3Qjj0Ud9eDoTG0fu4JxyAr/tE=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
|
||||
github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE=
|
||||
github.com/ulikunitz/xz v0.5.4 h1:zATC2OoZ8H1TZll3FpbX+ikwmadbO699PE06cIkm9oU=
|
||||
github.com/ulikunitz/xz v0.5.4/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
||||
github.com/xanzy/ssh-agent v0.2.0 h1:Adglfbi5p9Z0BmK2oKU9nTG+zKfniSfnaMYB+ULd+Ro=
|
||||
github.com/xanzy/ssh-agent v0.2.0/go.mod h1:0NyE30eGUDliuLEHJgYte/zncp2zdTStcOnWhgSqHD8=
|
||||
golang.org/x/crypto v0.0.0-20180808211826-de0752318171 h1:vYogbvSFj2YXcjQxFHu/rASSOt9sLytpCaSkiwQ135I=
|
||||
golang.org/x/crypto v0.0.0-20180808211826-de0752318171/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/net v0.0.0-20180811021610-c39426892332 h1:efGso+ep0DjyCBJPjvoz0HI6UldX4Md2F1rZFe1ir0E=
|
||||
golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0 h1:8H8QZJ30plJyIVj60H3lr8TZGIq2Fh3Cyrs/ZNg1foU=
|
||||
golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.0.0-20171214130843-f21a4dfb5e38/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
|
||||
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 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/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e h1:D5TXcfTk7xF7hvieo4QErS3qqCB4teTffacDWr7CI+0=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be h1:QAcqgptGM8IQBC9K/RC4o+O9YmqEm0diQn9QmZw/0mU=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-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/src-d/go-billy.v4 v4.2.0 h1:VGbrP1EsYxtvVPEiHui+4//imr4E5MGEFLx66bQtusg=
|
||||
gopkg.in/src-d/go-billy.v4 v4.2.0/go.mod h1:ZHSF0JP+7oD97194otDUCD7Ofbk63+xFcfWP5bT6h+Q=
|
||||
gopkg.in/src-d/go-git.v4 v4.0.0-20180807092216-43d17e14b714 h1:+wM2BGgQ1znCKBexOB4OrGVSDw8mtKNUSq3wqxZhi/k=
|
||||
gopkg.in/src-d/go-git.v4 v4.0.0-20180807092216-43d17e14b714/go.mod h1:CzbUWqMn4pvmvndg3gnh5iZFmSsbhyhUWdI0IQ60AQo=
|
||||
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/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
|
||||
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
|
||||
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg=
|
||||
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
|
||||
gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE=
|
||||
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -9,6 +8,7 @@ import (
|
||||
"runtime"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/integrii/flaggy"
|
||||
"github.com/jesseduffield/lazygit/pkg/app"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
)
|
||||
@@ -18,10 +18,6 @@ var (
|
||||
version = "unversioned"
|
||||
date string
|
||||
buildSource = "unknown"
|
||||
|
||||
configFlag = flag.Bool("config", false, "Print the current default config")
|
||||
debuggingFlag = flag.Bool("debug", false, "a boolean")
|
||||
versionFlag = flag.Bool("v", false, "Print the current version")
|
||||
)
|
||||
|
||||
func projectPath(path string) string {
|
||||
@@ -30,17 +26,43 @@ func projectPath(path string) string {
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if *versionFlag {
|
||||
flaggy.DefaultParser.ShowVersionWithVersionFlag = false
|
||||
|
||||
repoPath := "."
|
||||
flaggy.String(&repoPath, "p", "path", "Path of git repo")
|
||||
|
||||
dump := ""
|
||||
flaggy.AddPositionalValue(&dump, "gitargs", 1, false, "Todo file")
|
||||
flaggy.DefaultParser.PositionalFlags[0].Hidden = true
|
||||
|
||||
versionFlag := false
|
||||
flaggy.Bool(&versionFlag, "v", "version", "Print the current version")
|
||||
|
||||
debuggingFlag := false
|
||||
flaggy.Bool(&debuggingFlag, "d", "debug", "Run in debug mode with logging")
|
||||
|
||||
configFlag := false
|
||||
flaggy.Bool(&configFlag, "c", "config", "Print the current default config")
|
||||
|
||||
flaggy.Parse()
|
||||
|
||||
if versionFlag {
|
||||
fmt.Printf("commit=%s, build date=%s, build source=%s, version=%s, os=%s, arch=%s\n", commit, date, buildSource, version, runtime.GOOS, runtime.GOARCH)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if *configFlag {
|
||||
if configFlag {
|
||||
fmt.Printf("%s\n", config.GetDefaultConfig())
|
||||
os.Exit(0)
|
||||
}
|
||||
appConfig, err := config.NewAppConfig("lazygit", version, commit, date, buildSource, *debuggingFlag)
|
||||
|
||||
if repoPath != "." {
|
||||
if err := os.Chdir(repoPath); err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
appConfig, err := config.NewAppConfig("lazygit", version, commit, date, buildSource, debuggingFlag)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
@@ -52,6 +74,9 @@ func main() {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if errorMessage, known := app.KnownError(err); known {
|
||||
log.Fatal(errorMessage)
|
||||
}
|
||||
newErr := errors.Wrap(err, 0)
|
||||
stackTrace := newErr.ErrorStack()
|
||||
app.Log.Error(stackTrace)
|
||||
|
||||
+62
-6
@@ -1,6 +1,7 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -8,12 +9,12 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/heroku/rollrus"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/updates"
|
||||
"github.com/jesseduffield/rollrus"
|
||||
"github.com/shibukawa/configdir"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -32,9 +33,15 @@ type App struct {
|
||||
ClientContext string
|
||||
}
|
||||
|
||||
type errorMapping struct {
|
||||
originalError string
|
||||
newError string
|
||||
}
|
||||
|
||||
func newProductionLogger(config config.AppConfigurer) *logrus.Logger {
|
||||
log := logrus.New()
|
||||
log.Out = ioutil.Discard
|
||||
log.SetLevel(logrus.ErrorLevel)
|
||||
return log
|
||||
}
|
||||
|
||||
@@ -44,8 +51,18 @@ func globalConfigDir() string {
|
||||
return configDir.Path
|
||||
}
|
||||
|
||||
func getLogLevel() logrus.Level {
|
||||
strLevel := os.Getenv("LOG_LEVEL")
|
||||
level, err := logrus.ParseLevel(strLevel)
|
||||
if err != nil {
|
||||
return logrus.DebugLevel
|
||||
}
|
||||
return level
|
||||
}
|
||||
|
||||
func newDevelopmentLogger(config config.AppConfigurer) *logrus.Logger {
|
||||
log := logrus.New()
|
||||
log.SetLevel(getLogLevel())
|
||||
file, err := os.OpenFile(filepath.Join(globalConfigDir(), "development.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
panic("unable to log to file") // TODO: don't panic (also, remove this call to the `panic` function)
|
||||
@@ -103,12 +120,13 @@ func NewApp(config config.AppConfigurer) (*App, error) {
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
|
||||
if err := app.setupRepo(); err != nil {
|
||||
return app, err
|
||||
}
|
||||
|
||||
app.GitCommand, err = commands.NewGitCommand(app.Log, app.OSCommand, app.Tr, app.Config)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "Not a git repository") {
|
||||
fmt.Println("Not in a git repository. Use `git init` to create a new one")
|
||||
os.Exit(1)
|
||||
}
|
||||
return app, err
|
||||
}
|
||||
app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config, app.Updater)
|
||||
@@ -118,6 +136,24 @@ func NewApp(config config.AppConfigurer) (*App, error) {
|
||||
return app, nil
|
||||
}
|
||||
|
||||
func (app *App) setupRepo() error {
|
||||
// if we are not in a git repo, we ask if we want to `git init`
|
||||
if err := app.OSCommand.RunCommand("git status"); err != nil {
|
||||
if !strings.Contains(err.Error(), "Not a git repository") {
|
||||
return err
|
||||
}
|
||||
fmt.Print(app.Tr.SLocalize("CreateRepo"))
|
||||
response, _ := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||
if strings.Trim(response, " \n") != "y" {
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := app.OSCommand.RunCommand("git init"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *App) Run() error {
|
||||
if app.ClientContext == "INTERACTIVE_REBASE" {
|
||||
return app.Rebase()
|
||||
@@ -127,7 +163,8 @@ func (app *App) Run() error {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
return app.Gui.RunWithSubprocesses()
|
||||
err := app.Gui.RunWithSubprocesses()
|
||||
return err
|
||||
}
|
||||
|
||||
// Rebase contains logic for when we've been run in demon mode, meaning we've
|
||||
@@ -161,3 +198,22 @@ func (app *App) Close() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// KnownError takes an error and tells us whether it's an error that we know about where we can print a nicely formatted version of it rather than panicking with a stack trace
|
||||
func (app *App) KnownError(err error) (string, bool) {
|
||||
errorMessage := err.Error()
|
||||
|
||||
mappings := []errorMapping{
|
||||
{
|
||||
originalError: "fatal: not a git repository (or any of the parent directories): .git",
|
||||
newError: app.Tr.SLocalize("notARepository"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, mapping := range mappings {
|
||||
if strings.Contains(errorMessage, mapping.originalError) {
|
||||
return mapping.newError, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
@@ -18,7 +20,7 @@ type Branch struct {
|
||||
Selected bool
|
||||
}
|
||||
|
||||
// GetDisplayStrings returns the dispaly string of branch
|
||||
// GetDisplayStrings returns the display string of branch
|
||||
func (b *Branch) GetDisplayStrings(isFocused bool) []string {
|
||||
displayName := utils.ColoredString(b.Name, b.GetColor())
|
||||
if isFocused && b.Selected && b.Pushables != "" && b.Pullables != "" {
|
||||
@@ -38,7 +40,7 @@ func (b *Branch) GetColor() color.Attribute {
|
||||
case "hotfix":
|
||||
return color.FgRed
|
||||
default:
|
||||
return color.FgWhite
|
||||
return theme.DefaultTextColor
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package git
|
||||
package commands
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -26,28 +25,28 @@ import (
|
||||
// BranchListBuilder returns a list of Branch objects for the current repo
|
||||
type BranchListBuilder struct {
|
||||
Log *logrus.Entry
|
||||
GitCommand *commands.GitCommand
|
||||
GitCommand *GitCommand
|
||||
}
|
||||
|
||||
// NewBranchListBuilder builds a new branch list builder
|
||||
func NewBranchListBuilder(log *logrus.Entry, gitCommand *commands.GitCommand) (*BranchListBuilder, error) {
|
||||
func NewBranchListBuilder(log *logrus.Entry, gitCommand *GitCommand) (*BranchListBuilder, error) {
|
||||
return &BranchListBuilder{
|
||||
Log: log,
|
||||
GitCommand: gitCommand,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *BranchListBuilder) obtainCurrentBranch() *commands.Branch {
|
||||
func (b *BranchListBuilder) obtainCurrentBranch() *Branch {
|
||||
branchName, err := b.GitCommand.CurrentBranchName()
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
return &commands.Branch{Name: strings.TrimSpace(branchName)}
|
||||
return &Branch{Name: strings.TrimSpace(branchName)}
|
||||
}
|
||||
|
||||
func (b *BranchListBuilder) obtainReflogBranches() []*commands.Branch {
|
||||
branches := make([]*commands.Branch, 0)
|
||||
func (b *BranchListBuilder) obtainReflogBranches() []*Branch {
|
||||
branches := make([]*Branch, 0)
|
||||
rawString, err := b.GitCommand.OSCommand.RunCommandWithOutput("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD")
|
||||
if err != nil {
|
||||
return branches
|
||||
@@ -57,29 +56,29 @@ func (b *BranchListBuilder) obtainReflogBranches() []*commands.Branch {
|
||||
for _, line := range branchLines {
|
||||
timeNumber, timeUnit, branchName := branchInfoFromLine(line)
|
||||
timeUnit = abbreviatedTimeUnit(timeUnit)
|
||||
branch := &commands.Branch{Name: branchName, Recency: timeNumber + timeUnit}
|
||||
branch := &Branch{Name: branchName, Recency: timeNumber + timeUnit}
|
||||
branches = append(branches, branch)
|
||||
}
|
||||
return uniqueByName(branches)
|
||||
}
|
||||
|
||||
func (b *BranchListBuilder) obtainSafeBranches() []*commands.Branch {
|
||||
branches := make([]*commands.Branch, 0)
|
||||
func (b *BranchListBuilder) obtainSafeBranches() []*Branch {
|
||||
branches := make([]*Branch, 0)
|
||||
|
||||
bIter, err := b.GitCommand.Repo.Branches()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = bIter.ForEach(func(b *plumbing.Reference) error {
|
||||
bIter.ForEach(func(b *plumbing.Reference) error {
|
||||
name := b.Name().Short()
|
||||
branches = append(branches, &commands.Branch{Name: name})
|
||||
branches = append(branches, &Branch{Name: name})
|
||||
return nil
|
||||
})
|
||||
|
||||
return branches
|
||||
}
|
||||
|
||||
func (b *BranchListBuilder) appendNewBranches(finalBranches, newBranches, existingBranches []*commands.Branch, included bool) []*commands.Branch {
|
||||
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)
|
||||
@@ -88,7 +87,7 @@ func (b *BranchListBuilder) appendNewBranches(finalBranches, newBranches, existi
|
||||
return finalBranches
|
||||
}
|
||||
|
||||
func sanitisedReflogName(reflogBranch *commands.Branch, safeBranches []*commands.Branch) string {
|
||||
func sanitisedReflogName(reflogBranch *Branch, safeBranches []*Branch) string {
|
||||
for _, safeBranch := range safeBranches {
|
||||
if strings.ToLower(safeBranch.Name) == strings.ToLower(reflogBranch.Name) {
|
||||
return safeBranch.Name
|
||||
@@ -98,8 +97,8 @@ func sanitisedReflogName(reflogBranch *commands.Branch, safeBranches []*commands
|
||||
}
|
||||
|
||||
// Build the list of branches for the current repo
|
||||
func (b *BranchListBuilder) Build() []*commands.Branch {
|
||||
branches := make([]*commands.Branch, 0)
|
||||
func (b *BranchListBuilder) Build() []*Branch {
|
||||
branches := make([]*Branch, 0)
|
||||
head := b.obtainCurrentBranch()
|
||||
safeBranches := b.obtainSafeBranches()
|
||||
|
||||
@@ -112,7 +111,7 @@ func (b *BranchListBuilder) Build() []*commands.Branch {
|
||||
branches = b.appendNewBranches(branches, safeBranches, branches, false)
|
||||
|
||||
if len(branches) == 0 || branches[0].Name != head.Name {
|
||||
branches = append([]*commands.Branch{head}, branches...)
|
||||
branches = append([]*Branch{head}, branches...)
|
||||
}
|
||||
|
||||
branches[0].Recency = " *"
|
||||
@@ -120,7 +119,7 @@ func (b *BranchListBuilder) Build() []*commands.Branch {
|
||||
return branches
|
||||
}
|
||||
|
||||
func branchIncluded(branchName string, branches []*commands.Branch) bool {
|
||||
func branchIncluded(branchName string, branches []*Branch) bool {
|
||||
for _, existingBranch := range branches {
|
||||
if strings.ToLower(existingBranch.Name) == strings.ToLower(branchName) {
|
||||
return true
|
||||
@@ -129,8 +128,8 @@ func branchIncluded(branchName string, branches []*commands.Branch) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func uniqueByName(branches []*commands.Branch) []*commands.Branch {
|
||||
finalBranches := make([]*commands.Branch, 0)
|
||||
func uniqueByName(branches []*Branch) []*Branch {
|
||||
finalBranches := make([]*Branch, 0)
|
||||
for _, branch := range branches {
|
||||
if branchIncluded(branch.Name, finalBranches) {
|
||||
continue
|
||||
@@ -2,6 +2,7 @@ package commands
|
||||
|
||||
import (
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -9,7 +10,7 @@ import (
|
||||
type Commit struct {
|
||||
Sha string
|
||||
Name string
|
||||
Status string // one of "unpushed", "pushed", "merged", or "rebasing"
|
||||
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
|
||||
@@ -22,7 +23,8 @@ func (c *Commit) GetDisplayStrings(isFocused bool) []string {
|
||||
green := color.New(color.FgGreen)
|
||||
blue := color.New(color.FgBlue)
|
||||
cyan := color.New(color.FgCyan)
|
||||
white := color.New(color.FgWhite)
|
||||
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,
|
||||
@@ -39,8 +41,10 @@ func (c *Commit) GetDisplayStrings(isFocused bool) []string {
|
||||
shaColor = green
|
||||
case "rebasing":
|
||||
shaColor = blue
|
||||
case "selected":
|
||||
shaColor = magenta
|
||||
default:
|
||||
shaColor = white
|
||||
shaColor = defaultColor
|
||||
}
|
||||
|
||||
if c.Copied {
|
||||
@@ -52,5 +56,5 @@ func (c *Commit) GetDisplayStrings(isFocused bool) []string {
|
||||
actionString = cyan.Sprint(utils.WithPadding(c.Action, 7)) + " "
|
||||
}
|
||||
|
||||
return []string{shaColor.Sprint(c.Sha), actionString + white.Sprint(c.Name)}
|
||||
return []string{shaColor.Sprint(c.Sha), actionString + defaultColor.Sprint(c.Name)}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
)
|
||||
|
||||
// CommitFile : A git commit file
|
||||
type CommitFile struct {
|
||||
Sha string
|
||||
Name string
|
||||
DisplayString string
|
||||
Status int // one of 'WHOLE' 'PART' 'NONE'
|
||||
}
|
||||
|
||||
const (
|
||||
// UNSELECTED is for when the commit file has not been added to the patch in any way
|
||||
UNSELECTED = iota
|
||||
// WHOLE is for when you want to add the whole diff of a file to the patch,
|
||||
// including e.g. if it was deleted
|
||||
WHOLE = iota
|
||||
// PART is for when you're only talking about specific lines that have been modified
|
||||
PART
|
||||
)
|
||||
|
||||
// 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)}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package git
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -27,27 +26,29 @@ import (
|
||||
// CommitListBuilder returns a list of Branch objects for the current repo
|
||||
type CommitListBuilder struct {
|
||||
Log *logrus.Entry
|
||||
GitCommand *commands.GitCommand
|
||||
OSCommand *commands.OSCommand
|
||||
GitCommand *GitCommand
|
||||
OSCommand *OSCommand
|
||||
Tr *i18n.Localizer
|
||||
CherryPickedCommits []*commands.Commit
|
||||
CherryPickedCommits []*Commit
|
||||
DiffEntries []*Commit
|
||||
}
|
||||
|
||||
// NewCommitListBuilder builds a new commit list builder
|
||||
func NewCommitListBuilder(log *logrus.Entry, gitCommand *commands.GitCommand, osCommand *commands.OSCommand, tr *i18n.Localizer, cherryPickedCommits []*commands.Commit) (*CommitListBuilder, error) {
|
||||
func NewCommitListBuilder(log *logrus.Entry, gitCommand *GitCommand, osCommand *OSCommand, tr *i18n.Localizer, cherryPickedCommits []*Commit, diffEntries []*Commit) (*CommitListBuilder, error) {
|
||||
return &CommitListBuilder{
|
||||
Log: log,
|
||||
GitCommand: gitCommand,
|
||||
OSCommand: osCommand,
|
||||
Tr: tr,
|
||||
CherryPickedCommits: cherryPickedCommits,
|
||||
DiffEntries: diffEntries,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetCommits obtains the commits of the current branch
|
||||
func (c *CommitListBuilder) GetCommits() ([]*commands.Commit, error) {
|
||||
commits := []*commands.Commit{}
|
||||
var rebasingCommits []*commands.Commit
|
||||
func (c *CommitListBuilder) GetCommits() ([]*Commit, error) {
|
||||
commits := []*Commit{}
|
||||
var rebasingCommits []*Commit
|
||||
rebaseMode, err := c.GitCommand.RebaseMode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -72,7 +73,7 @@ func (c *CommitListBuilder) GetCommits() ([]*commands.Commit, error) {
|
||||
sha := splitLine[0]
|
||||
_, unpushed := unpushedCommits[sha]
|
||||
status := map[bool]string{true: "unpushed", false: "pushed"}[unpushed]
|
||||
commits = append(commits, &commands.Commit{
|
||||
commits = append(commits, &Commit{
|
||||
Sha: sha,
|
||||
Name: strings.Join(splitLine[1:], " "),
|
||||
Status: status,
|
||||
@@ -96,11 +97,19 @@ func (c *CommitListBuilder) GetCommits() ([]*commands.Commit, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, commit := range commits {
|
||||
for _, entry := range c.DiffEntries {
|
||||
if entry.Sha == commit.Sha {
|
||||
commit.Status = "selected"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return commits, nil
|
||||
}
|
||||
|
||||
// getRebasingCommits obtains the commits that we're in the process of rebasing
|
||||
func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*commands.Commit, error) {
|
||||
func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*Commit, error) {
|
||||
switch rebaseMode {
|
||||
case "normal":
|
||||
return c.getNormalRebasingCommits()
|
||||
@@ -111,17 +120,17 @@ func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*commands.C
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CommitListBuilder) getNormalRebasingCommits() ([]*commands.Commit, error) {
|
||||
func (c *CommitListBuilder) getNormalRebasingCommits() ([]*Commit, error) {
|
||||
rewrittenCount := 0
|
||||
bytesContent, err := ioutil.ReadFile(".git/rebase-apply/rewritten")
|
||||
bytesContent, err := ioutil.ReadFile(fmt.Sprintf("%s/rebase-apply/rewritten", c.GitCommand.DotGitDir))
|
||||
if err == nil {
|
||||
content := string(bytesContent)
|
||||
rewrittenCount = len(strings.Split(content, "\n"))
|
||||
}
|
||||
|
||||
// we know we're rebasing, so lets get all the files whose names have numbers
|
||||
commits := []*commands.Commit{}
|
||||
err = filepath.Walk(".git/rebase-apply", func(path string, f os.FileInfo, err error) error {
|
||||
commits := []*Commit{}
|
||||
err = filepath.Walk(fmt.Sprintf("%s/rebase-apply", c.GitCommand.DotGitDir), func(path string, f os.FileInfo, err error) error {
|
||||
if rewrittenCount > 0 {
|
||||
rewrittenCount--
|
||||
return nil
|
||||
@@ -142,7 +151,7 @@ func (c *CommitListBuilder) getNormalRebasingCommits() ([]*commands.Commit, erro
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
commits = append([]*commands.Commit{commit}, commits...)
|
||||
commits = append([]*Commit{commit}, commits...)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
@@ -164,22 +173,22 @@ func (c *CommitListBuilder) getNormalRebasingCommits() ([]*commands.Commit, erro
|
||||
// getInteractiveRebasingCommits takes our git-rebase-todo and our git-rebase-todo.backup files
|
||||
// and extracts out the sha and names of commits that we still have to go
|
||||
// in the rebase:
|
||||
func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*commands.Commit, error) {
|
||||
bytesContent, err := ioutil.ReadFile(".git/rebase-merge/git-rebase-todo")
|
||||
func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*Commit, error) {
|
||||
bytesContent, err := ioutil.ReadFile(fmt.Sprintf("%s/rebase-merge/git-rebase-todo", c.GitCommand.DotGitDir))
|
||||
if err != nil {
|
||||
c.Log.Info(fmt.Sprintf("error occured reading git-rebase-todo: %s", err.Error()))
|
||||
c.Log.Info(fmt.Sprintf("error occurred reading git-rebase-todo: %s", err.Error()))
|
||||
// we assume an error means the file doesn't exist so we just return
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
commits := []*commands.Commit{}
|
||||
commits := []*Commit{}
|
||||
lines := strings.Split(string(bytesContent), "\n")
|
||||
for _, line := range lines {
|
||||
if line == "" || line == "noop" {
|
||||
return commits, nil
|
||||
}
|
||||
splitLine := strings.Split(line, " ")
|
||||
commits = append([]*commands.Commit{{
|
||||
commits = append([]*Commit{{
|
||||
Sha: splitLine[1][0:7],
|
||||
Name: strings.Join(splitLine[2:], " "),
|
||||
Status: "rebasing",
|
||||
@@ -195,18 +204,18 @@ func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*commands.Commit,
|
||||
// From: Lazygit Tester <test@example.com>
|
||||
// Date: Wed, 5 Dec 2018 21:03:23 +1100
|
||||
// Subject: second commit on master
|
||||
func (c *CommitListBuilder) commitFromPatch(content string) (*commands.Commit, error) {
|
||||
func (c *CommitListBuilder) commitFromPatch(content string) (*Commit, error) {
|
||||
lines := strings.Split(content, "\n")
|
||||
sha := strings.Split(lines[0], " ")[1][0:7]
|
||||
name := strings.TrimPrefix(lines[3], "Subject: ")
|
||||
return &commands.Commit{
|
||||
return &Commit{
|
||||
Sha: sha,
|
||||
Name: name,
|
||||
Status: "rebasing",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *CommitListBuilder) setCommitMergedStatuses(commits []*commands.Commit) ([]*commands.Commit, error) {
|
||||
func (c *CommitListBuilder) setCommitMergedStatuses(commits []*Commit) ([]*Commit, error) {
|
||||
ancestor, err := c.getMergeBase()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -229,7 +238,7 @@ func (c *CommitListBuilder) setCommitMergedStatuses(commits []*commands.Commit)
|
||||
return commits, nil
|
||||
}
|
||||
|
||||
func (c *CommitListBuilder) setCommitCherryPickStatuses(commits []*commands.Commit) ([]*commands.Commit, error) {
|
||||
func (c *CommitListBuilder) setCommitCherryPickStatuses(commits []*Commit) ([]*Commit, error) {
|
||||
for _, commit := range commits {
|
||||
for _, cherryPickedCommit := range c.CherryPickedCommits {
|
||||
if commit.Sha == cherryPickedCommit.Sha {
|
||||
@@ -251,10 +260,8 @@ func (c *CommitListBuilder) getMergeBase() (string, error) {
|
||||
baseBranch = "develop"
|
||||
}
|
||||
|
||||
output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git merge-base HEAD %s", baseBranch))
|
||||
if err != nil {
|
||||
// swallowing error because it's not a big deal; probably because there are no commits yet
|
||||
}
|
||||
// swallowing error because it's not a big deal; probably because there are no commits yet
|
||||
output, _ := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git merge-base HEAD %s", baseBranch))
|
||||
return output, nil
|
||||
}
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
package git
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// NewDummyCommitListBuilder creates a new dummy CommitListBuilder for testing
|
||||
func NewDummyCommitListBuilder() *CommitListBuilder {
|
||||
osCommand := commands.NewDummyOSCommand()
|
||||
osCommand := NewDummyOSCommand()
|
||||
|
||||
return &CommitListBuilder{
|
||||
Log: commands.NewDummyLog(),
|
||||
GitCommand: commands.NewDummyGitCommandWithOSCommand(osCommand),
|
||||
Log: NewDummyLog(),
|
||||
GitCommand: NewDummyGitCommandWithOSCommand(osCommand),
|
||||
OSCommand: osCommand,
|
||||
Tr: i18n.NewLocalizer(commands.NewDummyLog()),
|
||||
CherryPickedCommits: []*commands.Commit{},
|
||||
Tr: i18n.NewLocalizer(NewDummyLog()),
|
||||
CherryPickedCommits: []*Commit{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,7 +198,7 @@ func TestCommitListBuilderGetCommits(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func([]*commands.Commit, error)
|
||||
test func([]*Commit, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
@@ -225,7 +224,7 @@ func TestCommitListBuilderGetCommits(t *testing.T) {
|
||||
|
||||
return nil
|
||||
},
|
||||
func(commits []*commands.Commit, err error) {
|
||||
func(commits []*Commit, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, commits, 0)
|
||||
},
|
||||
@@ -252,10 +251,10 @@ func TestCommitListBuilderGetCommits(t *testing.T) {
|
||||
|
||||
return nil
|
||||
},
|
||||
func(commits []*commands.Commit, err error) {
|
||||
func(commits []*Commit, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, commits, 2)
|
||||
assert.EqualValues(t, []*commands.Commit{
|
||||
assert.EqualValues(t, []*Commit{
|
||||
{
|
||||
Sha: "8a2bb0e",
|
||||
Name: "commit 1",
|
||||
@@ -298,7 +297,7 @@ func TestCommitListBuilderGetCommits(t *testing.T) {
|
||||
|
||||
return nil
|
||||
},
|
||||
func(commits []*commands.Commit, err error) {
|
||||
func(commits []*Commit, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.Len(t, commits, 0)
|
||||
},
|
||||
@@ -0,0 +1,14 @@
|
||||
package commands
|
||||
|
||||
import "github.com/go-errors/errors"
|
||||
|
||||
// WrapError wraps an error for the sake of showing a stack trace at the top level
|
||||
// the go-errors package, for some reason, does not return nil when you try to wrap
|
||||
// a non-error, so we're just doing it here
|
||||
func WrapError(err error) error {
|
||||
if err == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return errors.Wrap(err, 0)
|
||||
}
|
||||
@@ -5,14 +5,12 @@ package commands
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"os"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
"github.com/jesseduffield/pty"
|
||||
"github.com/mgutz/str"
|
||||
)
|
||||
|
||||
// RunCommandWithOutputLiveWrapper runs a command and return every word that gets written in stdout
|
||||
@@ -20,10 +18,7 @@ import (
|
||||
// As return of output you need to give a string that will be written to stdin
|
||||
// NOTE: If the return data is empty it won't written anything to stdin
|
||||
func RunCommandWithOutputLiveWrapper(c *OSCommand, command string, output func(string) string) error {
|
||||
splitCmd := str.ToArgv(command)
|
||||
cmd := c.command(splitCmd[0], splitCmd[1:]...)
|
||||
|
||||
cmd.Env = os.Environ()
|
||||
cmd := c.ExecutableFromString(command)
|
||||
cmd.Env = append(cmd.Env, "LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8")
|
||||
|
||||
var stderr bytes.Buffer
|
||||
|
||||
@@ -14,6 +14,7 @@ type File struct {
|
||||
HasInlineMergeConflicts bool
|
||||
DisplayString string
|
||||
Type string // one of 'file', 'directory', and 'other'
|
||||
ShortStatus string // e.g. 'AD', ' A', 'M ', '??'
|
||||
}
|
||||
|
||||
// GetDisplayStrings returns the display string of a file
|
||||
|
||||
+330
-74
@@ -5,7 +5,9 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mgutz/str"
|
||||
|
||||
@@ -25,18 +27,18 @@ func verifyInGitRepo(runCmd func(string) error) error {
|
||||
|
||||
func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir func(string) error) error {
|
||||
for {
|
||||
f, err := stat(".git")
|
||||
_, err := stat(".git")
|
||||
|
||||
if err == nil && f.IsDir() {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !os.IsNotExist(err) {
|
||||
return errors.Wrap(err, 0)
|
||||
return WrapError(err)
|
||||
}
|
||||
|
||||
if err = chdir(".."); err != nil {
|
||||
return errors.Wrap(err, 0)
|
||||
return WrapError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,15 +65,18 @@ func setupRepositoryAndWorktree(openGitRepository func(string) (*gogit.Repositor
|
||||
|
||||
// GitCommand is our main git interface
|
||||
type GitCommand struct {
|
||||
Log *logrus.Entry
|
||||
OSCommand *OSCommand
|
||||
Worktree *gogit.Worktree
|
||||
Repo *gogit.Repository
|
||||
Tr *i18n.Localizer
|
||||
Config config.AppConfigurer
|
||||
getGlobalGitConfig func(string) (string, error)
|
||||
getLocalGitConfig func(string) (string, error)
|
||||
removeFile func(string) error
|
||||
Log *logrus.Entry
|
||||
OSCommand *OSCommand
|
||||
Worktree *gogit.Worktree
|
||||
Repo *gogit.Repository
|
||||
Tr *i18n.Localizer
|
||||
Config config.AppConfigurer
|
||||
getGlobalGitConfig func(string) (string, error)
|
||||
getLocalGitConfig func(string) (string, error)
|
||||
removeFile func(string) error
|
||||
DotGitDir string
|
||||
onSuccessfulContinue func() error
|
||||
PatchManager *PatchManager
|
||||
}
|
||||
|
||||
// NewGitCommand it runs git commands
|
||||
@@ -99,7 +104,12 @@ func NewGitCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localizer,
|
||||
}
|
||||
}
|
||||
|
||||
return &GitCommand{
|
||||
dotGitDir, err := findDotGitDir(os.Stat, ioutil.ReadFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gitCommand := &GitCommand{
|
||||
Log: log,
|
||||
OSCommand: osCommand,
|
||||
Tr: tr,
|
||||
@@ -109,10 +119,36 @@ func NewGitCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localizer,
|
||||
getGlobalGitConfig: gitconfig.Global,
|
||||
getLocalGitConfig: gitconfig.Local,
|
||||
removeFile: os.RemoveAll,
|
||||
}, nil
|
||||
DotGitDir: dotGitDir,
|
||||
}
|
||||
|
||||
gitCommand.PatchManager = NewPatchManager(log, gitCommand.ApplyPatch)
|
||||
|
||||
return gitCommand, nil
|
||||
}
|
||||
|
||||
// GetStashEntries stash entryies
|
||||
func findDotGitDir(stat func(string) (os.FileInfo, error), readFile func(filename string) ([]byte, error)) (string, error) {
|
||||
f, err := stat(".git")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if f.IsDir() {
|
||||
return ".git", nil
|
||||
}
|
||||
|
||||
fileBytes, err := readFile(".git")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fileContent := string(fileBytes)
|
||||
if !strings.HasPrefix(fileContent, "gitdir: ") {
|
||||
return "", errors.New(".git is a file which suggests we are in a submodule but the file's contents do not contain a gitdir pointing to the actual .git directory")
|
||||
}
|
||||
return strings.TrimSpace(strings.TrimPrefix(fileContent, "gitdir: ")), nil
|
||||
}
|
||||
|
||||
// GetStashEntries stash entries
|
||||
func (c *GitCommand) GetStashEntries() []*StashEntry {
|
||||
rawString, _ := c.OSCommand.RunCommandWithOutput("git stash list --pretty='%gs'")
|
||||
stashEntries := []*StashEntry{}
|
||||
@@ -148,6 +184,8 @@ func (c *GitCommand) GetStatusFiles() []*File {
|
||||
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"
|
||||
|
||||
file := &File{
|
||||
Name: filename,
|
||||
@@ -156,9 +194,10 @@ func (c *GitCommand) GetStatusFiles() []*File {
|
||||
HasUnstagedChanges: unstagedChange != " ",
|
||||
Tracked: !untracked,
|
||||
Deleted: unstagedChange == "D" || stagedChange == "D",
|
||||
HasMergeConflicts: change == "UU" || change == "AA" || change == "DU",
|
||||
HasInlineMergeConflicts: change == "UU" || change == "AA",
|
||||
HasMergeConflicts: hasMergeConflicts,
|
||||
HasInlineMergeConflicts: hasInlineMergeConflicts,
|
||||
Type: c.OSCommand.FileType(filename),
|
||||
ShortStatus: change,
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
@@ -217,11 +256,11 @@ func includesInt(list []int, a int) bool {
|
||||
|
||||
// ResetAndClean removes all unstaged changes and removes all untracked files
|
||||
func (c *GitCommand) ResetAndClean() error {
|
||||
if err := c.OSCommand.RunCommand("git reset --hard HEAD"); err != nil {
|
||||
if err := c.ResetHardHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.OSCommand.RunCommand("git clean -fd")
|
||||
return c.RemoveUntrackedFiles()
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetCurrentBranchUpstreamDifferenceCount() (string, string) {
|
||||
@@ -274,8 +313,8 @@ func (c *GitCommand) Fetch(unamePassQuestion func(string) string, canAskForCrede
|
||||
}
|
||||
|
||||
// ResetToCommit reset to commit
|
||||
func (c *GitCommand) ResetToCommit(sha string) error {
|
||||
return c.OSCommand.RunCommand(fmt.Sprintf("git reset %s", sha))
|
||||
func (c *GitCommand) ResetToCommit(sha string, strength string) error {
|
||||
return c.OSCommand.RunCommand(fmt.Sprintf("git reset --%s %s", strength, sha))
|
||||
}
|
||||
|
||||
// NewBranch create new branch
|
||||
@@ -334,8 +373,8 @@ func (c *GitCommand) usingGpg() bool {
|
||||
}
|
||||
|
||||
// Commit commits to git
|
||||
func (c *GitCommand) Commit(message string) (*exec.Cmd, error) {
|
||||
command := fmt.Sprintf("git commit -m %s", c.OSCommand.Quote(message))
|
||||
func (c *GitCommand) Commit(message string, flags string) (*exec.Cmd, error) {
|
||||
command := fmt.Sprintf("git commit %s -m %s", flags, c.OSCommand.Quote(message))
|
||||
if c.usingGpg() {
|
||||
return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command), nil
|
||||
}
|
||||
@@ -345,7 +384,7 @@ func (c *GitCommand) Commit(message string) (*exec.Cmd, error) {
|
||||
|
||||
// AmendHead amends HEAD with whatever is staged in your working tree
|
||||
func (c *GitCommand) AmendHead() (*exec.Cmd, error) {
|
||||
command := "git commit --amend --no-edit"
|
||||
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
|
||||
}
|
||||
@@ -359,13 +398,18 @@ func (c *GitCommand) Pull(ask func(string) string) error {
|
||||
}
|
||||
|
||||
// Push pushes to a branch
|
||||
func (c *GitCommand) Push(branchName string, force bool, ask func(string) string) error {
|
||||
func (c *GitCommand) Push(branchName string, force bool, upstream string, ask func(string) string) error {
|
||||
forceFlag := ""
|
||||
if force {
|
||||
forceFlag = "--force-with-lease "
|
||||
forceFlag = "--force-with-lease"
|
||||
}
|
||||
|
||||
cmd := fmt.Sprintf("git push %s-u origin %s", forceFlag, branchName)
|
||||
setUpstreamArg := ""
|
||||
if upstream != "" {
|
||||
setUpstreamArg = "--set-upstream " + upstream
|
||||
}
|
||||
|
||||
cmd := fmt.Sprintf("git push %s %s", forceFlag, setUpstreamArg)
|
||||
return c.OSCommand.DetectUnamePass(cmd, ask)
|
||||
}
|
||||
|
||||
@@ -408,7 +452,7 @@ 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 --short")
|
||||
return c.OSCommand.RunCommandWithOutput("git status --untracked-files=all --porcelain")
|
||||
}
|
||||
|
||||
// IsInMergeState states whether we are still mid-merge
|
||||
@@ -423,14 +467,14 @@ func (c *GitCommand) IsInMergeState() (bool, error) {
|
||||
// RebaseMode returns "" for non-rebase mode, "normal" for normal rebase
|
||||
// and "interactive" for interactive rebase
|
||||
func (c *GitCommand) RebaseMode() (string, error) {
|
||||
exists, err := c.OSCommand.FileExists(".git/rebase-apply")
|
||||
exists, err := c.OSCommand.FileExists(fmt.Sprintf("%s/rebase-apply", c.DotGitDir))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if exists {
|
||||
return "normal", nil
|
||||
}
|
||||
exists, err = c.OSCommand.FileExists(".git/rebase-merge")
|
||||
exists, err = c.OSCommand.FileExists(fmt.Sprintf("%s/rebase-merge", c.DotGitDir))
|
||||
if exists {
|
||||
return "interactive", err
|
||||
} else {
|
||||
@@ -438,19 +482,25 @@ func (c *GitCommand) RebaseMode() (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveFile directly
|
||||
func (c *GitCommand) RemoveFile(file *File) error {
|
||||
// DiscardAllFileChanges directly
|
||||
func (c *GitCommand) DiscardAllFileChanges(file *File) error {
|
||||
// if the file isn't tracked, we assume you want to delete it
|
||||
quotedFileName := c.OSCommand.Quote(file.Name)
|
||||
if file.HasStagedChanges {
|
||||
if file.HasStagedChanges || file.HasMergeConflicts {
|
||||
if err := c.OSCommand.RunCommand(fmt.Sprintf("git reset -- %s", quotedFileName)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !file.Tracked {
|
||||
return c.removeFile(file.Name)
|
||||
}
|
||||
// if the file is tracked, we assume you want to just check it out
|
||||
return c.DiscardUnstagedFileChanges(file)
|
||||
}
|
||||
|
||||
// DiscardUnstagedFileChanges directly
|
||||
func (c *GitCommand) DiscardUnstagedFileChanges(file *File) error {
|
||||
quotedFileName := c.OSCommand.Quote(file.Name)
|
||||
return c.OSCommand.RunCommand(fmt.Sprintf("git checkout -- %s", quotedFileName))
|
||||
}
|
||||
|
||||
@@ -463,12 +513,6 @@ func (c *GitCommand) Checkout(branch string, force bool) error {
|
||||
return c.OSCommand.RunCommand(fmt.Sprintf("git checkout %s %s", forceArg, branch))
|
||||
}
|
||||
|
||||
// AddPatch prepares a subprocess for adding a patch by patch
|
||||
// this will eventually be swapped out for a better solution inside the Gui
|
||||
func (c *GitCommand) AddPatch(filename string) *exec.Cmd {
|
||||
return c.OSCommand.PrepareSubProcess("git", "add", "--patch", c.OSCommand.Quote(filename))
|
||||
}
|
||||
|
||||
// PrepareCommitSubProcess prepares a subprocess for `git commit`
|
||||
func (c *GitCommand) PrepareCommitSubProcess() *exec.Cmd {
|
||||
return c.OSCommand.PrepareSubProcess("git", "commit")
|
||||
@@ -493,7 +537,7 @@ func (c *GitCommand) Ignore(filename string) error {
|
||||
|
||||
// Show shows the diff of a commit
|
||||
func (c *GitCommand) Show(sha string) (string, error) {
|
||||
show, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git show --color %s", sha))
|
||||
show, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git show --color --no-renames %s", sha))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -545,13 +589,13 @@ func (c *GitCommand) CheckRemoteBranchExists(branch *Branch) bool {
|
||||
}
|
||||
|
||||
// Diff returns the diff of a file
|
||||
func (c *GitCommand) Diff(file *File, plain bool) string {
|
||||
func (c *GitCommand) Diff(file *File, plain bool, cached bool) string {
|
||||
cachedArg := ""
|
||||
trackedArg := "--"
|
||||
colorArg := "--color"
|
||||
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 file.HasStagedChanges && !file.HasUnstagedChanges {
|
||||
if cached {
|
||||
cachedArg = "--cached"
|
||||
}
|
||||
if !file.Tracked && !file.HasStagedChanges {
|
||||
@@ -568,16 +612,19 @@ func (c *GitCommand) Diff(file *File, plain bool) string {
|
||||
return s
|
||||
}
|
||||
|
||||
func (c *GitCommand) ApplyPatch(patch string) (string, error) {
|
||||
filename, err := c.OSCommand.CreateTempFile("patch", patch)
|
||||
if err != nil {
|
||||
c.Log.Error(err)
|
||||
return "", err
|
||||
func (c *GitCommand) ApplyPatch(patch string, flags ...string) error {
|
||||
c.Log.Warn(patch)
|
||||
filepath := filepath.Join(c.Config.GetUserConfigDir(), utils.GetCurrentRepoName(), time.Now().Format(time.StampNano)+".patch")
|
||||
if err := c.OSCommand.CreateFileWithContent(filepath, patch); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() { _ = c.OSCommand.RemoveFile(filename) }()
|
||||
flagStr := ""
|
||||
for _, flag := range flags {
|
||||
flagStr += " --" + flag
|
||||
}
|
||||
|
||||
return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git apply --cached %s", c.OSCommand.Quote(filename)))
|
||||
return c.OSCommand.RunCommand(fmt.Sprintf("git apply %s %s", flagStr, c.OSCommand.Quote(filepath)))
|
||||
}
|
||||
|
||||
func (c *GitCommand) FastForward(branchName string) error {
|
||||
@@ -588,7 +635,7 @@ func (c *GitCommand) FastForward(branchName string) error {
|
||||
func (c *GitCommand) RunSkipEditorCommand(command string) error {
|
||||
cmd := c.OSCommand.ExecutableFromString(command)
|
||||
cmd.Env = append(
|
||||
os.Environ(),
|
||||
cmd.Env,
|
||||
"LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY",
|
||||
"EDITOR="+c.OSCommand.GetLazygitPath(),
|
||||
)
|
||||
@@ -598,22 +645,38 @@ func (c *GitCommand) RunSkipEditorCommand(command string) error {
|
||||
// GenericMerge takes a commandType of "merge" or "rebase" and a command of "abort", "skip" or "continue"
|
||||
// By default we skip the editor in the case where a commit will be made
|
||||
func (c *GitCommand) GenericMerge(commandType string, command string) error {
|
||||
return c.RunSkipEditorCommand(
|
||||
err := c.RunSkipEditorCommand(
|
||||
fmt.Sprintf(
|
||||
"git %s --%s",
|
||||
commandType,
|
||||
command,
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// sometimes we need to do a sequence of things in a rebase but the user needs to
|
||||
// fix merge conflicts along the way. When this happens we queue up the next step
|
||||
// so that after the next successful rebase continue we can continue from where we left off
|
||||
if commandType == "rebase" && command == "continue" && c.onSuccessfulContinue != nil {
|
||||
f := c.onSuccessfulContinue
|
||||
c.onSuccessfulContinue = nil
|
||||
return f()
|
||||
}
|
||||
if command == "abort" {
|
||||
c.onSuccessfulContinue = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *GitCommand) RewordCommit(commits []*Commit, index int) (*exec.Cmd, error) {
|
||||
todo, err := c.GenerateGenericRebaseTodo(commits, index, "reword")
|
||||
todo, sha, err := c.GenerateGenericRebaseTodo(commits, index, "reword")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.PrepareInteractiveRebaseCommand(commits[index+1].Sha, todo, false)
|
||||
return c.PrepareInteractiveRebaseCommand(sha, todo, false)
|
||||
}
|
||||
|
||||
func (c *GitCommand) MoveCommitDown(commits []*Commit, index int) error {
|
||||
@@ -638,12 +701,12 @@ func (c *GitCommand) MoveCommitDown(commits []*Commit, index int) error {
|
||||
}
|
||||
|
||||
func (c *GitCommand) InteractiveRebase(commits []*Commit, index int, action string) error {
|
||||
todo, err := c.GenerateGenericRebaseTodo(commits, index, action)
|
||||
todo, sha, err := c.GenerateGenericRebaseTodo(commits, index, action)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd, err := c.PrepareInteractiveRebaseCommand(commits[index+1].Sha, todo, true)
|
||||
cmd, err := c.PrepareInteractiveRebaseCommand(sha, todo, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -658,11 +721,11 @@ func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string
|
||||
ex := c.OSCommand.GetLazygitPath()
|
||||
|
||||
debug := "FALSE"
|
||||
if c.OSCommand.Config.GetDebug() == true {
|
||||
if c.OSCommand.Config.GetDebug() {
|
||||
debug = "TRUE"
|
||||
}
|
||||
|
||||
splitCmd := str.ToArgv(fmt.Sprintf("git rebase --interactive --autostash %s", baseSha))
|
||||
splitCmd := str.ToArgv(fmt.Sprintf("git rebase --interactive --autostash --keep-empty --rebase-merges %s", baseSha))
|
||||
|
||||
cmd := c.OSCommand.command(splitCmd[0], splitCmd[1:]...)
|
||||
|
||||
@@ -697,38 +760,45 @@ func (c *GitCommand) SoftReset(baseSha string) error {
|
||||
return c.OSCommand.RunCommand("git reset --soft " + baseSha)
|
||||
}
|
||||
|
||||
func (c *GitCommand) GenerateGenericRebaseTodo(commits []*Commit, index int, action string) (string, error) {
|
||||
if len(commits) <= index+1 {
|
||||
// assuming they aren't picking the bottom commit
|
||||
return "", errors.New(c.Tr.SLocalize("CannotRebaseOntoFirstCommit"))
|
||||
func (c *GitCommand) GenerateGenericRebaseTodo(commits []*Commit, actionIndex int, action string) (string, string, error) {
|
||||
baseIndex := actionIndex + 1
|
||||
|
||||
if len(commits) <= baseIndex {
|
||||
return "", "", errors.New(c.Tr.SLocalize("CannotRebaseOntoFirstCommit"))
|
||||
}
|
||||
|
||||
if action == "squash" || action == "fixup" {
|
||||
baseIndex++
|
||||
|
||||
if len(commits) <= baseIndex {
|
||||
return "", "", errors.New(c.Tr.SLocalize("CannotSquashOntoSecondCommit"))
|
||||
}
|
||||
}
|
||||
|
||||
todo := ""
|
||||
for i, commit := range commits[0 : index+1] {
|
||||
for i, commit := range commits[0:baseIndex] {
|
||||
a := "pick"
|
||||
if i == index {
|
||||
if i == actionIndex {
|
||||
a = action
|
||||
}
|
||||
todo = a + " " + commit.Sha + " " + commit.Name + "\n" + todo
|
||||
}
|
||||
return todo, nil
|
||||
|
||||
return todo, commits[baseIndex].Sha, nil
|
||||
}
|
||||
|
||||
// AmendTo amends the given commit with whatever files are staged
|
||||
func (c *GitCommand) AmendTo(sha string) error {
|
||||
if err := c.OSCommand.RunCommand(fmt.Sprintf("git commit --fixup=%s", sha)); err != nil {
|
||||
if err := c.CreateFixupCommit(sha); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.RunSkipEditorCommand(
|
||||
fmt.Sprintf(
|
||||
"git rebase --interactive --autostash --autosquash %s^", sha,
|
||||
),
|
||||
)
|
||||
|
||||
return c.SquashAllAboveFixupCommits(sha)
|
||||
}
|
||||
|
||||
// EditRebaseTodo sets the action at a given index in the git-rebase-todo file
|
||||
func (c *GitCommand) EditRebaseTodo(index int, action string) error {
|
||||
fileName := ".git/rebase-merge/git-rebase-todo"
|
||||
fileName := fmt.Sprintf("%s/rebase-merge/git-rebase-todo", c.DotGitDir)
|
||||
bytes, err := ioutil.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -760,7 +830,7 @@ func (c *GitCommand) getTodoCommitCount(content []string) int {
|
||||
|
||||
// MoveTodoDown moves a rebase todo item down by one position
|
||||
func (c *GitCommand) MoveTodoDown(index int) error {
|
||||
fileName := ".git/rebase-merge/git-rebase-todo"
|
||||
fileName := fmt.Sprintf("%s/rebase-merge/git-rebase-todo", c.DotGitDir)
|
||||
bytes, err := ioutil.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -796,3 +866,189 @@ func (c *GitCommand) CherryPickCommits(commits []*Commit) error {
|
||||
|
||||
return c.OSCommand.RunPreparedCommand(cmd)
|
||||
}
|
||||
|
||||
// GetCommitFiles get the specified commit files
|
||||
func (c *GitCommand) GetCommitFiles(commitSha string, patchManager *PatchManager) ([]*CommitFile, error) {
|
||||
cmd := fmt.Sprintf("git show --pretty= --name-only --no-renames %s", commitSha)
|
||||
files, err := c.OSCommand.RunCommandWithOutput(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
commitFiles := make([]*CommitFile, 0)
|
||||
|
||||
for _, file := range strings.Split(strings.TrimRight(files, "\n"), "\n") {
|
||||
status := UNSELECTED
|
||||
if patchManager != nil && patchManager.CommitSha == commitSha {
|
||||
status = patchManager.GetFileStatus(file)
|
||||
}
|
||||
|
||||
commitFiles = append(commitFiles, &CommitFile{
|
||||
Sha: commitSha,
|
||||
Name: file,
|
||||
DisplayString: file,
|
||||
Status: status,
|
||||
})
|
||||
}
|
||||
|
||||
return commitFiles, nil
|
||||
}
|
||||
|
||||
// ShowCommitFile get the diff of specified commit file
|
||||
func (c *GitCommand) ShowCommitFile(commitSha, fileName string, plain bool) (string, error) {
|
||||
colorArg := "--color"
|
||||
if plain {
|
||||
colorArg = ""
|
||||
}
|
||||
cmd := fmt.Sprintf("git show --no-renames %s %s -- %s", colorArg, commitSha, fileName)
|
||||
return c.OSCommand.RunCommandWithOutput(cmd)
|
||||
}
|
||||
|
||||
// CheckoutFile checks out the file for the given commit
|
||||
func (c *GitCommand) CheckoutFile(commitSha, fileName string) error {
|
||||
cmd := fmt.Sprintf("git checkout %s %s", commitSha, fileName)
|
||||
return c.OSCommand.RunCommand(cmd)
|
||||
}
|
||||
|
||||
// DiscardOldFileChanges discards changes to a file from an old commit
|
||||
func (c *GitCommand) DiscardOldFileChanges(commits []*Commit, commitIndex int, fileName string) error {
|
||||
if err := c.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check if file exists in previous commit (this command returns an error if the file doesn't exist)
|
||||
if err := c.OSCommand.RunCommand(fmt.Sprintf("git cat-file -e HEAD^:%s", fileName)); err != nil {
|
||||
if err := c.OSCommand.Remove(fileName); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.StageFile(fileName); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := c.CheckoutFile("HEAD^", fileName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// amend the commit
|
||||
cmd, err := c.AmendHead()
|
||||
if cmd != nil {
|
||||
return errors.New("received unexpected pointer to cmd")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// continue
|
||||
return c.GenericMerge("rebase", "continue")
|
||||
}
|
||||
|
||||
// DiscardAnyUnstagedFileChanges discards any unstages file changes via `git checkout -- .`
|
||||
func (c *GitCommand) DiscardAnyUnstagedFileChanges() error {
|
||||
return c.OSCommand.RunCommand("git checkout -- .")
|
||||
}
|
||||
|
||||
// RemoveUntrackedFiles runs `git clean -fd`
|
||||
func (c *GitCommand) RemoveUntrackedFiles() error {
|
||||
return c.OSCommand.RunCommand("git clean -fd")
|
||||
}
|
||||
|
||||
// ResetHardHead runs `git reset --hard HEAD`
|
||||
func (c *GitCommand) ResetHardHead() error {
|
||||
return c.OSCommand.RunCommand("git reset --hard HEAD")
|
||||
}
|
||||
|
||||
// ResetSoftHead runs `git reset --soft HEAD`
|
||||
func (c *GitCommand) ResetSoftHead() error {
|
||||
return c.OSCommand.RunCommand("git reset --soft HEAD")
|
||||
}
|
||||
|
||||
// DiffCommits show diff between commits
|
||||
func (c *GitCommand) DiffCommits(sha1, sha2 string) (string, error) {
|
||||
cmd := fmt.Sprintf("git diff --color %s %s", sha1, sha2)
|
||||
return c.OSCommand.RunCommandWithOutput(cmd)
|
||||
}
|
||||
|
||||
// CreateFixupCommit creates a commit that fixes up a previous commit
|
||||
func (c *GitCommand) CreateFixupCommit(sha string) error {
|
||||
cmd := fmt.Sprintf("git commit --fixup=%s", sha)
|
||||
return c.OSCommand.RunCommand(cmd)
|
||||
}
|
||||
|
||||
// SquashAllAboveFixupCommits squashes all fixup! commits above the given one
|
||||
func (c *GitCommand) SquashAllAboveFixupCommits(sha string) error {
|
||||
return c.RunSkipEditorCommand(
|
||||
fmt.Sprintf(
|
||||
"git rebase --interactive --autostash --autosquash %s^",
|
||||
sha,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// StashSaveStagedChanges stashes only the currently staged changes. This takes a few steps
|
||||
// shoutouts to Joe on https://stackoverflow.com/questions/14759748/stashing-only-staged-changes-in-git-is-it-possible
|
||||
func (c *GitCommand) StashSaveStagedChanges(message string) error {
|
||||
|
||||
if err := c.OSCommand.RunCommand("git stash --keep-index"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.StashSave(message); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.OSCommand.RunCommand("git stash apply stash@{1}"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.OSCommand.PipeCommands("git stash show -p", "git apply -R"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.OSCommand.RunCommand("git stash drop stash@{1}"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if you had staged an untracked file, that will now appear as 'AD' in git status
|
||||
// meaning it's deleted in your working tree but added in your index. Given that it's
|
||||
// now safely stashed, we need to remove it.
|
||||
files := c.GetStatusFiles()
|
||||
for _, file := range files {
|
||||
if file.ShortStatus == "AD" {
|
||||
if err := c.UnStageFile(file.Name, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeginInteractiveRebaseForCommit starts an interactive rebase to edit the current
|
||||
// commit and pick all others. After this you'll want to call `c.GenericMerge("rebase", "continue")`
|
||||
func (c *GitCommand) BeginInteractiveRebaseForCommit(commits []*Commit, commitIndex int) error {
|
||||
if len(commits)-1 < commitIndex {
|
||||
return errors.New("index outside of range of commits")
|
||||
}
|
||||
|
||||
// we can make this GPG thing possible it just means we need to do this in two parts:
|
||||
// one where we handle the possibility of a credential request, and the other
|
||||
// where we continue the rebase
|
||||
if c.usingGpg() {
|
||||
return errors.New(c.Tr.SLocalize("DisabledForGPG"))
|
||||
}
|
||||
|
||||
todo, sha, err := c.GenerateGenericRebaseTodo(commits, commitIndex, "edit")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd, err := c.PrepareInteractiveRebaseCommand(sha, todo, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.OSCommand.RunPreparedCommand(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+614
-77
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -355,52 +356,72 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return exec.Command(
|
||||
"echo",
|
||||
"MM file1.txt\nA file3.txt\nAM file2.txt\n?? file4.txt",
|
||||
"MM file1.txt\nA file3.txt\nAM file2.txt\n?? file4.txt\nUU file5.txt",
|
||||
)
|
||||
},
|
||||
func(files []*File) {
|
||||
assert.Len(t, files, 4)
|
||||
assert.Len(t, files, 5)
|
||||
|
||||
expected := []*File{
|
||||
{
|
||||
Name: "file1.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
DisplayString: "MM file1.txt",
|
||||
Type: "other",
|
||||
Name: "file1.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "MM file1.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "MM",
|
||||
},
|
||||
{
|
||||
Name: "file3.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: false,
|
||||
Tracked: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
DisplayString: "A file3.txt",
|
||||
Type: "other",
|
||||
Name: "file3.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: false,
|
||||
Tracked: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "A file3.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "A ",
|
||||
},
|
||||
{
|
||||
Name: "file2.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
DisplayString: "AM file2.txt",
|
||||
Type: "other",
|
||||
Name: "file2.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "AM file2.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "AM",
|
||||
},
|
||||
{
|
||||
Name: "file4.txt",
|
||||
HasStagedChanges: false,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
DisplayString: "?? file4.txt",
|
||||
Type: "other",
|
||||
Name: "file4.txt",
|
||||
HasStagedChanges: false,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "?? file4.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "??",
|
||||
},
|
||||
{
|
||||
Name: "file5.txt",
|
||||
HasStagedChanges: false,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: true,
|
||||
HasInlineMergeConflicts: true,
|
||||
DisplayString: "UU file5.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "UU",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -616,12 +637,12 @@ func TestGitCommandResetToCommit(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"reset", "78976bc"}, args)
|
||||
assert.EqualValues(t, []string{"reset", "--hard", "78976bc"}, args)
|
||||
|
||||
return exec.Command("echo")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.ResetToCommit("78976bc"))
|
||||
assert.NoError(t, gitCmd.ResetToCommit("78976bc", "hard"))
|
||||
}
|
||||
|
||||
// TestGitCommandNewBranch is a function.
|
||||
@@ -801,6 +822,7 @@ func TestGitCommandCommit(t *testing.T) {
|
||||
command func(string, ...string) *exec.Cmd
|
||||
getGlobalGitConfig func(string) (string, error)
|
||||
test func(*exec.Cmd, error)
|
||||
flags string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
@@ -808,7 +830,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")
|
||||
},
|
||||
@@ -819,6 +841,7 @@ func TestGitCommandCommit(t *testing.T) {
|
||||
assert.NotNil(t, cmd)
|
||||
assert.Nil(t, err)
|
||||
},
|
||||
"",
|
||||
},
|
||||
{
|
||||
"Commit without using gpg",
|
||||
@@ -835,6 +858,24 @@ func TestGitCommandCommit(t *testing.T) {
|
||||
assert.Nil(t, cmd)
|
||||
assert.Nil(t, err)
|
||||
},
|
||||
"",
|
||||
},
|
||||
{
|
||||
"Commit with --no-verify flag",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"commit", "--no-verify", "-m", "test"}, args)
|
||||
|
||||
return exec.Command("echo")
|
||||
},
|
||||
func(string) (string, error) {
|
||||
return "false", nil
|
||||
},
|
||||
func(cmd *exec.Cmd, err error) {
|
||||
assert.Nil(t, cmd)
|
||||
assert.Nil(t, err)
|
||||
},
|
||||
"--no-verify",
|
||||
},
|
||||
{
|
||||
"Commit without using gpg with an error",
|
||||
@@ -851,6 +892,7 @@ func TestGitCommandCommit(t *testing.T) {
|
||||
assert.Nil(t, cmd)
|
||||
assert.Error(t, err)
|
||||
},
|
||||
"",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -859,7 +901,7 @@ func TestGitCommandCommit(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.getGlobalGitConfig = s.getGlobalGitConfig
|
||||
gitCmd.OSCommand.command = s.command
|
||||
s.test(gitCmd.Commit("test"))
|
||||
s.test(gitCmd.Commit("test", s.flags))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -878,7 +920,7 @@ func TestGitCommandAmendHead(t *testing.T) {
|
||||
"Amend commit using gpg",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "bash", cmd)
|
||||
assert.EqualValues(t, []string{"-c", "git commit --amend --no-edit"}, args)
|
||||
assert.EqualValues(t, []string{"-c", "git commit --amend --no-edit --allow-empty"}, args)
|
||||
|
||||
return exec.Command("echo")
|
||||
},
|
||||
@@ -894,7 +936,7 @@ func TestGitCommandAmendHead(t *testing.T) {
|
||||
"Amend commit without using gpg",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"commit", "--amend", "--no-edit"}, args)
|
||||
assert.EqualValues(t, []string{"commit", "--amend", "--no-edit", "--allow-empty"}, args)
|
||||
|
||||
return exec.Command("echo")
|
||||
},
|
||||
@@ -910,7 +952,7 @@ func TestGitCommandAmendHead(t *testing.T) {
|
||||
"Amend commit without using gpg with an error",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"commit", "--amend", "--no-edit"}, args)
|
||||
assert.EqualValues(t, []string{"commit", "--amend", "--no-edit", "--allow-empty"}, args)
|
||||
|
||||
return exec.Command("test")
|
||||
},
|
||||
@@ -948,7 +990,7 @@ func TestGitCommandPush(t *testing.T) {
|
||||
"Push with force disabled",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push", "-u", "origin", "test"}, args)
|
||||
assert.EqualValues(t, []string{"push"}, args)
|
||||
|
||||
return exec.Command("echo")
|
||||
},
|
||||
@@ -961,7 +1003,7 @@ func TestGitCommandPush(t *testing.T) {
|
||||
"Push with force enabled",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push", "--force-with-lease", "-u", "origin", "test"}, args)
|
||||
assert.EqualValues(t, []string{"push", "--force-with-lease"}, args)
|
||||
|
||||
return exec.Command("echo")
|
||||
},
|
||||
@@ -974,7 +1016,7 @@ func TestGitCommandPush(t *testing.T) {
|
||||
"Push with an error occurring",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push", "-u", "origin", "test"}, args)
|
||||
assert.EqualValues(t, []string{"push"}, args)
|
||||
return exec.Command("test")
|
||||
},
|
||||
false,
|
||||
@@ -988,7 +1030,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)
|
||||
@@ -1140,8 +1182,8 @@ func TestGitCommandIsInMergeState(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandRemoveFile is a function.
|
||||
func TestGitCommandRemoveFile(t *testing.T) {
|
||||
// TestGitCommandDiscardAllFileChanges is a function.
|
||||
func TestGitCommandDiscardAllFileChanges(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func() (func(string, ...string) *exec.Cmd, *[][]string)
|
||||
@@ -1252,7 +1294,7 @@ func TestGitCommandRemoveFile(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
"Reset and checkout",
|
||||
"Reset and checkout staged changes",
|
||||
func() (func(string, ...string) *exec.Cmd, *[][]string) {
|
||||
cmdsCalled := [][]string{}
|
||||
return func(cmd string, args ...string) *exec.Cmd {
|
||||
@@ -1278,6 +1320,33 @@ func TestGitCommandRemoveFile(t *testing.T) {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
"Reset and checkout merge conflicts",
|
||||
func() (func(string, ...string) *exec.Cmd, *[][]string) {
|
||||
cmdsCalled := [][]string{}
|
||||
return func(cmd string, args ...string) *exec.Cmd {
|
||||
cmdsCalled = append(cmdsCalled, args)
|
||||
|
||||
return exec.Command("echo")
|
||||
}, &cmdsCalled
|
||||
},
|
||||
func(cmdsCalled *[][]string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, *cmdsCalled, 2)
|
||||
assert.EqualValues(t, *cmdsCalled, [][]string{
|
||||
{"reset", "--", "test"},
|
||||
{"checkout", "--", "test"},
|
||||
})
|
||||
},
|
||||
&File{
|
||||
Name: "test",
|
||||
Tracked: true,
|
||||
HasMergeConflicts: true,
|
||||
},
|
||||
func(string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
"Reset and remove",
|
||||
func() (func(string, ...string) *exec.Cmd, *[][]string) {
|
||||
@@ -1337,7 +1406,7 @@ func TestGitCommandRemoveFile(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.command, cmdsCalled = s.command()
|
||||
gitCmd.removeFile = s.removeFile
|
||||
s.test(cmdsCalled, gitCmd.RemoveFile(s.file))
|
||||
s.test(cmdsCalled, gitCmd.DiscardAllFileChanges(s.file))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1357,7 +1426,7 @@ func TestGitCommandShow(t *testing.T) {
|
||||
"456abcde",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git show --color 456abcde",
|
||||
Expect: "git show --color --no-renames 456abcde",
|
||||
Replace: "echo \"commit ccc771d8b13d5b0d4635db4463556366470fd4f6\nblah\"",
|
||||
},
|
||||
{
|
||||
@@ -1375,7 +1444,7 @@ func TestGitCommandShow(t *testing.T) {
|
||||
"456abcde",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git show --color 456abcde",
|
||||
Expect: "git show --color --no-renames 456abcde",
|
||||
Replace: "echo \"commit ccc771d8b13d5b0d4635db4463556366470fd4f6\nMerge: 1a6a69a 3b51d7c\"",
|
||||
},
|
||||
{
|
||||
@@ -1470,6 +1539,7 @@ func TestGitCommandDiff(t *testing.T) {
|
||||
command func(string, ...string) *exec.Cmd
|
||||
file *File
|
||||
plain bool
|
||||
cached bool
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
@@ -1487,9 +1557,26 @@ func TestGitCommandDiff(t *testing.T) {
|
||||
Tracked: true,
|
||||
},
|
||||
false,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Default case",
|
||||
"cached",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"diff", "--color", "--cached", "--", "test.txt"}, args)
|
||||
|
||||
return exec.Command("echo")
|
||||
},
|
||||
&File{
|
||||
Name: "test.txt",
|
||||
HasStagedChanges: false,
|
||||
Tracked: true,
|
||||
},
|
||||
false,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"plain",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"diff", "--", "test.txt"}, args)
|
||||
@@ -1502,21 +1589,6 @@ func TestGitCommandDiff(t *testing.T) {
|
||||
Tracked: true,
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"All changes staged",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"diff", "--color", "--cached", "--", "test.txt"}, args)
|
||||
|
||||
return exec.Command("echo")
|
||||
},
|
||||
&File{
|
||||
Name: "test.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: false,
|
||||
Tracked: true,
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
@@ -1533,6 +1605,7 @@ func TestGitCommandDiff(t *testing.T) {
|
||||
Tracked: false,
|
||||
},
|
||||
false,
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1540,7 +1613,7 @@ func TestGitCommandDiff(t *testing.T) {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.command = s.command
|
||||
gitCmd.Diff(s.file, s.plain)
|
||||
gitCmd.Diff(s.file, s.plain, s.cached)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1612,7 +1685,7 @@ func TestGitCommandApplyPatch(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(string, error)
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
@@ -1629,9 +1702,8 @@ func TestGitCommandApplyPatch(t *testing.T) {
|
||||
|
||||
return exec.Command("echo", "done")
|
||||
},
|
||||
func(output string, err error) {
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "done\n", output)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1651,7 +1723,7 @@ func TestGitCommandApplyPatch(t *testing.T) {
|
||||
|
||||
return exec.Command("test")
|
||||
},
|
||||
func(output string, err error) {
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
@@ -1661,7 +1733,7 @@ func TestGitCommandApplyPatch(t *testing.T) {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.command = s.command
|
||||
s.test(gitCmd.ApplyPatch("test"))
|
||||
s.test(gitCmd.ApplyPatch("test", "cached"))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1681,7 +1753,7 @@ func TestGitCommandRebaseBranch(t *testing.T) {
|
||||
"master",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git rebase --interactive --autostash master",
|
||||
Expect: "git rebase --interactive --autostash --keep-empty --rebase-merges master",
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
@@ -1694,7 +1766,7 @@ func TestGitCommandRebaseBranch(t *testing.T) {
|
||||
"master",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git rebase --interactive --autostash master",
|
||||
Expect: "git rebase --interactive --autostash --keep-empty --rebase-merges master",
|
||||
Replace: "test",
|
||||
},
|
||||
}),
|
||||
@@ -1707,7 +1779,472 @@ func TestGitCommandRebaseBranch(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
gitCmd.OSCommand.command = s.command
|
||||
s.test(gitCmd.RebaseBranch(s.arg))
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.command = s.command
|
||||
s.test(gitCmd.RebaseBranch(s.arg))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandCheckoutFile is a function.
|
||||
func TestGitCommandCheckoutFile(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
commitSha string
|
||||
fileName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"typical case",
|
||||
"11af912",
|
||||
"test999.txt",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git checkout 11af912 test999.txt",
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"returns error if there is one",
|
||||
"11af912",
|
||||
"test999.txt",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git checkout 11af912 test999.txt",
|
||||
Replace: "test",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.command = s.command
|
||||
s.test(gitCmd.CheckoutFile(s.commitSha, s.fileName))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandDiscardOldFileChanges is a function.
|
||||
func TestGitCommandDiscardOldFileChanges(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
getLocalGitConfig func(string) (string, error)
|
||||
commits []*Commit
|
||||
commitIndex int
|
||||
fileName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"returns error when index outside of range of commits",
|
||||
func(string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
[]*Commit{},
|
||||
0,
|
||||
"test999.txt",
|
||||
nil,
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"returns error when using gpg",
|
||||
func(string) (string, error) {
|
||||
return "true", nil
|
||||
},
|
||||
[]*Commit{{Name: "commit", Sha: "123456"}},
|
||||
0,
|
||||
"test999.txt",
|
||||
nil,
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"checks out file if it already existed",
|
||||
func(string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
[]*Commit{
|
||||
{Name: "commit", Sha: "123456"},
|
||||
{Name: "commit2", Sha: "abcdef"},
|
||||
},
|
||||
0,
|
||||
"test999.txt",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git rebase --interactive --autostash --keep-empty --rebase-merges abcdef",
|
||||
Replace: "echo",
|
||||
},
|
||||
{
|
||||
Expect: "git cat-file -e HEAD^:test999.txt",
|
||||
Replace: "echo",
|
||||
},
|
||||
{
|
||||
Expect: "git checkout HEAD^ test999.txt",
|
||||
Replace: "echo",
|
||||
},
|
||||
{
|
||||
Expect: "git commit --amend --no-edit --allow-empty",
|
||||
Replace: "echo",
|
||||
},
|
||||
{
|
||||
Expect: "git rebase --continue",
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
// test for when the file was created within the commit requires a refactor to support proper mocks
|
||||
// currently we'd need to mock out the os.Remove function and that's gonna introduce tech debt
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.command = s.command
|
||||
gitCmd.getLocalGitConfig = s.getLocalGitConfig
|
||||
s.test(gitCmd.DiscardOldFileChanges(s.commits, s.commitIndex, s.fileName))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandShowCommitFile is a function.
|
||||
func TestGitCommandShowCommitFile(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
commitSha string
|
||||
fileName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(string, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
"123456",
|
||||
"hello.txt",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git show --no-renames 123456 -- hello.txt",
|
||||
Replace: "echo -n hello",
|
||||
},
|
||||
}),
|
||||
func(str string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hello", str)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.command = s.command
|
||||
s.test(gitCmd.ShowCommitFile(s.commitSha, s.fileName, true))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandGetCommitFiles is a function.
|
||||
func TestGitCommandGetCommitFiles(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
commitSha string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func([]*CommitFile, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
"123456",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git show --pretty= --name-only --no-renames 123456",
|
||||
Replace: "echo 'hello\nworld'",
|
||||
},
|
||||
}),
|
||||
func(commitFiles []*CommitFile, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []*CommitFile{
|
||||
{Sha: "123456", Name: "hello", DisplayString: "hello"},
|
||||
{Sha: "123456", Name: "world", DisplayString: "world"},
|
||||
}, commitFiles)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.command = s.command
|
||||
s.test(gitCmd.GetCommitFiles(s.commitSha, nil))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandDiscardUnstagedFileChanges is a function.
|
||||
func TestGitCommandDiscardUnstagedFileChanges(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
file *File
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
&File{Name: "test.txt"},
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: `git checkout -- "test.txt"`,
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.command = s.command
|
||||
s.test(gitCmd.DiscardUnstagedFileChanges(s.file))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandDiscardAnyUnstagedFileChanges is a function.
|
||||
func TestGitCommandDiscardAnyUnstagedFileChanges(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: `git checkout -- .`,
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.command = s.command
|
||||
s.test(gitCmd.DiscardAnyUnstagedFileChanges())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandRemoveUntrackedFiles is a function.
|
||||
func TestGitCommandRemoveUntrackedFiles(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: `git clean -fd`,
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.command = s.command
|
||||
s.test(gitCmd.RemoveUntrackedFiles())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandResetHardHead is a function.
|
||||
func TestGitCommandResetHardHead(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: `git reset --hard HEAD`,
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.command = s.command
|
||||
s.test(gitCmd.ResetHardHead())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandCreateFixupCommit is a function.
|
||||
func TestGitCommandCreateFixupCommit(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
sha string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
"12345",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: `git commit --fixup=12345`,
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.command = s.command
|
||||
s.test(gitCmd.CreateFixupCommit(s.sha))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindDotGitDir(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
stat func(string) (os.FileInfo, error)
|
||||
readFile func(filename string) ([]byte, error)
|
||||
test func(string, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
".git is a directory",
|
||||
func(dotGit string) (os.FileInfo, error) {
|
||||
assert.Equal(t, ".git", dotGit)
|
||||
return os.Stat("testdata/a_dir")
|
||||
},
|
||||
func(dotGit string) ([]byte, error) {
|
||||
assert.Fail(t, "readFile should not be called if .git is a directory")
|
||||
return nil, nil
|
||||
},
|
||||
func(gitDir string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, ".git", gitDir)
|
||||
},
|
||||
},
|
||||
{
|
||||
".git is a file",
|
||||
func(dotGit string) (os.FileInfo, error) {
|
||||
assert.Equal(t, ".git", dotGit)
|
||||
return os.Stat("testdata/a_file")
|
||||
},
|
||||
func(dotGit string) ([]byte, error) {
|
||||
assert.Equal(t, ".git", dotGit)
|
||||
return []byte("gitdir: blah\n"), nil
|
||||
},
|
||||
func(gitDir string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "blah", gitDir)
|
||||
},
|
||||
},
|
||||
{
|
||||
"os.Stat returns an error",
|
||||
func(dotGit string) (os.FileInfo, error) {
|
||||
assert.Equal(t, ".git", dotGit)
|
||||
return nil, errors.New("error")
|
||||
},
|
||||
func(dotGit string) ([]byte, error) {
|
||||
assert.Fail(t, "readFile should not be called os.Stat returns an error")
|
||||
return nil, nil
|
||||
},
|
||||
func(gitDir string, err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"readFile returns an error",
|
||||
func(dotGit string) (os.FileInfo, error) {
|
||||
assert.Equal(t, ".git", dotGit)
|
||||
return os.Stat("testdata/a_file")
|
||||
},
|
||||
func(dotGit string) ([]byte, error) {
|
||||
return nil, errors.New("error")
|
||||
},
|
||||
func(gitDir string, err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
s.test(findDotGitDir(s.stat, s.readFile))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+100
-13
@@ -7,6 +7,7 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
@@ -77,8 +78,9 @@ func (c *OSCommand) RunExecutable(cmd *exec.Cmd) error {
|
||||
// ExecutableFromString takes a string like `git status` and returns an executable command for it
|
||||
func (c *OSCommand) ExecutableFromString(commandStr string) *exec.Cmd {
|
||||
splitCmd := str.ToArgv(commandStr)
|
||||
c.Log.Info(splitCmd)
|
||||
return c.command(splitCmd[0], splitCmd[1:]...)
|
||||
cmd := c.command(splitCmd[0], splitCmd[1:]...)
|
||||
cmd.Env = append(os.Environ(), "GIT_OPTIONAL_LOCKS=0")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunCommandWithOutputLive runs RunCommandWithOutputLiveWrapper
|
||||
@@ -145,7 +147,7 @@ func sanitisedCommandOutput(output []byte, err error) (string, error) {
|
||||
// errors like 'exit status 1' are not very useful so we'll create an error
|
||||
// from the combined output
|
||||
if outputString == "" {
|
||||
return "", errors.Wrap(err, 0)
|
||||
return "", WrapError(err)
|
||||
}
|
||||
return outputString, errors.New(outputString)
|
||||
}
|
||||
@@ -200,8 +202,13 @@ func (c *OSCommand) EditFile(filename string) (*exec.Cmd, error) {
|
||||
}
|
||||
|
||||
// PrepareSubProcess iniPrepareSubProcessrocess then tells the Gui to switch to it
|
||||
// TODO: see if this needs to exist, given that ExecutableFromString does the same things
|
||||
func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *exec.Cmd {
|
||||
return c.command(cmdName, commandArgs...)
|
||||
cmd := c.command(cmdName, commandArgs...)
|
||||
if cmd != nil {
|
||||
cmd.Env = append(os.Environ(), "GIT_OPTIONAL_LOCKS=0")
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Quote wraps a message in platform-specific quotation marks
|
||||
@@ -224,13 +231,13 @@ func (c *OSCommand) Unquote(message string) string {
|
||||
func (c *OSCommand) AppendLineToFile(filename, line string) error {
|
||||
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, 0)
|
||||
return WrapError(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.WriteString("\n" + line)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, 0)
|
||||
return WrapError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -240,25 +247,40 @@ func (c *OSCommand) CreateTempFile(filename, content string) (string, error) {
|
||||
tmpfile, err := ioutil.TempFile("", filename)
|
||||
if err != nil {
|
||||
c.Log.Error(err)
|
||||
return "", errors.Wrap(err, 0)
|
||||
return "", WrapError(err)
|
||||
}
|
||||
|
||||
if _, err := tmpfile.WriteString(content); err != nil {
|
||||
c.Log.Error(err)
|
||||
return "", errors.Wrap(err, 0)
|
||||
return "", WrapError(err)
|
||||
}
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
c.Log.Error(err)
|
||||
return "", errors.Wrap(err, 0)
|
||||
return "", WrapError(err)
|
||||
}
|
||||
|
||||
return tmpfile.Name(), nil
|
||||
}
|
||||
|
||||
// RemoveFile removes a file at the specified path
|
||||
func (c *OSCommand) RemoveFile(filename string) error {
|
||||
err := os.Remove(filename)
|
||||
return errors.Wrap(err, 0)
|
||||
// CreateFileWithContent creates a file with the given content
|
||||
func (c *OSCommand) CreateFileWithContent(path string, content string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
|
||||
c.Log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(path, []byte(content), 0644); err != nil {
|
||||
c.Log.Error(err)
|
||||
return WrapError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove removes a file or directory at the specified path
|
||||
func (c *OSCommand) Remove(filename string) error {
|
||||
err := os.RemoveAll(filename)
|
||||
return WrapError(err)
|
||||
}
|
||||
|
||||
// FileExists checks whether a file exists at the specified path
|
||||
@@ -296,3 +318,68 @@ func (c *OSCommand) GetLazygitPath() string {
|
||||
}
|
||||
return filepath.ToSlash(ex)
|
||||
}
|
||||
|
||||
// RunCustomCommand returns the pointer to a custom command
|
||||
func (c *OSCommand) RunCustomCommand(command string) *exec.Cmd {
|
||||
return c.PrepareSubProcess(c.Platform.shell, c.Platform.shellArg, command)
|
||||
}
|
||||
|
||||
// PipeCommands runs a heap of commands and pipes their inputs/outputs together like A | B | C
|
||||
func (c *OSCommand) PipeCommands(commandStrings ...string) error {
|
||||
|
||||
cmds := make([]*exec.Cmd, len(commandStrings))
|
||||
|
||||
for i, str := range commandStrings {
|
||||
cmds[i] = c.ExecutableFromString(str)
|
||||
}
|
||||
|
||||
for i := 0; i < len(cmds)-1; i++ {
|
||||
stdout, err := cmds[i].StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmds[i+1].Stdin = stdout
|
||||
}
|
||||
|
||||
// keeping this here in case I adapt this code for some other purpose in the future
|
||||
// cmds[len(cmds)-1].Stdout = os.Stdout
|
||||
|
||||
finalErrors := []string{}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(cmds))
|
||||
|
||||
for _, cmd := range cmds {
|
||||
currentCmd := cmd
|
||||
go func() {
|
||||
stderr, err := currentCmd.StderrPipe()
|
||||
if err != nil {
|
||||
c.Log.Error(err)
|
||||
}
|
||||
|
||||
if err := currentCmd.Start(); err != nil {
|
||||
c.Log.Error(err)
|
||||
}
|
||||
|
||||
if b, err := ioutil.ReadAll(stderr); err == nil {
|
||||
if len(b) > 0 {
|
||||
finalErrors = append(finalErrors, string(b))
|
||||
}
|
||||
}
|
||||
|
||||
if err := currentCmd.Wait(); err != nil {
|
||||
c.Log.Error(err)
|
||||
}
|
||||
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if len(finalErrors) > 0 {
|
||||
return errors.New(strings.Join(finalErrors, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type fileInfo struct {
|
||||
mode int // one of WHOLE/PART
|
||||
includedLineIndices []int
|
||||
diff string
|
||||
}
|
||||
|
||||
type applyPatchFunc func(patch string, flags ...string) error
|
||||
|
||||
// PatchManager manages the building of a patch for a commit to be applied to another commit (or the working tree, or removed from the current commit)
|
||||
type PatchManager struct {
|
||||
CommitSha string
|
||||
fileInfoMap map[string]*fileInfo
|
||||
Log *logrus.Entry
|
||||
ApplyPatch applyPatchFunc
|
||||
}
|
||||
|
||||
// NewPatchManager returns a new PatchModifier
|
||||
func NewPatchManager(log *logrus.Entry, applyPatch applyPatchFunc) *PatchManager {
|
||||
return &PatchManager{
|
||||
Log: log,
|
||||
ApplyPatch: applyPatch,
|
||||
}
|
||||
}
|
||||
|
||||
// NewPatchManager returns a new PatchModifier
|
||||
func (p *PatchManager) Start(commitSha string, diffMap map[string]string) {
|
||||
p.CommitSha = commitSha
|
||||
p.fileInfoMap = map[string]*fileInfo{}
|
||||
for filename, diff := range diffMap {
|
||||
p.fileInfoMap[filename] = &fileInfo{
|
||||
mode: UNSELECTED,
|
||||
diff: diff,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PatchManager) AddFile(filename string) {
|
||||
p.fileInfoMap[filename].mode = WHOLE
|
||||
p.fileInfoMap[filename].includedLineIndices = nil
|
||||
}
|
||||
|
||||
func (p *PatchManager) RemoveFile(filename string) {
|
||||
p.fileInfoMap[filename].mode = UNSELECTED
|
||||
p.fileInfoMap[filename].includedLineIndices = nil
|
||||
}
|
||||
|
||||
func (p *PatchManager) ToggleFileWhole(filename string) {
|
||||
info := p.fileInfoMap[filename]
|
||||
switch info.mode {
|
||||
case UNSELECTED:
|
||||
p.AddFile(filename)
|
||||
case WHOLE:
|
||||
p.RemoveFile(filename)
|
||||
case PART:
|
||||
p.AddFile(filename)
|
||||
}
|
||||
}
|
||||
|
||||
func getIndicesForRange(first, last int) []int {
|
||||
indices := []int{}
|
||||
for i := first; i <= last; i++ {
|
||||
indices = append(indices, i)
|
||||
}
|
||||
return indices
|
||||
}
|
||||
|
||||
func (p *PatchManager) AddFileLineRange(filename string, firstLineIdx, lastLineIdx int) {
|
||||
info := p.fileInfoMap[filename]
|
||||
info.mode = PART
|
||||
info.includedLineIndices = utils.UnionInt(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx))
|
||||
}
|
||||
|
||||
func (p *PatchManager) RemoveFileLineRange(filename string, firstLineIdx, lastLineIdx int) {
|
||||
info := p.fileInfoMap[filename]
|
||||
info.mode = PART
|
||||
info.includedLineIndices = utils.DifferenceInt(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx))
|
||||
if len(info.includedLineIndices) == 0 {
|
||||
p.RemoveFile(filename)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PatchManager) RenderPlainPatchForFile(filename string, reverse bool, keepOriginalHeader bool) string {
|
||||
info := p.fileInfoMap[filename]
|
||||
if info == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch info.mode {
|
||||
case WHOLE:
|
||||
// use the whole diff
|
||||
// the reverse flag is only for part patches so we're ignoring it here
|
||||
return info.diff
|
||||
case PART:
|
||||
// generate a new diff with just the selected lines
|
||||
m := NewPatchModifier(p.Log, filename, info.diff)
|
||||
return m.ModifiedPatchForLines(info.includedLineIndices, reverse, keepOriginalHeader)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PatchManager) RenderPatchForFile(filename string, plain bool, reverse bool, keepOriginalHeader bool) string {
|
||||
patch := p.RenderPlainPatchForFile(filename, reverse, keepOriginalHeader)
|
||||
if plain {
|
||||
return patch
|
||||
}
|
||||
parser, err := NewPatchParser(p.Log, patch)
|
||||
if err != nil {
|
||||
// swallowing for now
|
||||
return ""
|
||||
}
|
||||
// not passing included lines because we don't want to see them in the secondary panel
|
||||
return parser.Render(-1, -1, nil)
|
||||
}
|
||||
|
||||
func (p *PatchManager) RenderEachFilePatch(plain bool) []string {
|
||||
// sort files by name then iterate through and render each patch
|
||||
filenames := make([]string, len(p.fileInfoMap))
|
||||
index := 0
|
||||
for filename := range p.fileInfoMap {
|
||||
filenames[index] = filename
|
||||
index++
|
||||
}
|
||||
|
||||
sort.Strings(filenames)
|
||||
output := []string{}
|
||||
for _, filename := range filenames {
|
||||
patch := p.RenderPatchForFile(filename, plain, false, true)
|
||||
if patch != "" {
|
||||
output = append(output, patch)
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func (p *PatchManager) RenderAggregatedPatchColored(plain bool) string {
|
||||
result := ""
|
||||
for _, patch := range p.RenderEachFilePatch(plain) {
|
||||
if patch != "" {
|
||||
result += patch + "\n"
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (p *PatchManager) GetFileStatus(filename string) int {
|
||||
info := p.fileInfoMap[filename]
|
||||
if info == nil {
|
||||
return UNSELECTED
|
||||
}
|
||||
return info.mode
|
||||
}
|
||||
|
||||
func (p *PatchManager) GetFileIncLineIndices(filename string) []int {
|
||||
info := p.fileInfoMap[filename]
|
||||
if info == nil {
|
||||
return []int{}
|
||||
}
|
||||
return info.includedLineIndices
|
||||
}
|
||||
|
||||
func (p *PatchManager) ApplyPatches(reverse bool) error {
|
||||
// for whole patches we'll apply the patch in reverse
|
||||
// but for part patches we'll apply a reverse patch forwards
|
||||
for filename, info := range p.fileInfoMap {
|
||||
if info.mode == UNSELECTED {
|
||||
continue
|
||||
}
|
||||
|
||||
applyFlags := []string{"index", "3way"}
|
||||
reverseOnGenerate := false
|
||||
if reverse {
|
||||
if info.mode == WHOLE {
|
||||
applyFlags = append(applyFlags, "reverse")
|
||||
} else {
|
||||
reverseOnGenerate = true
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
// first run we try with the original header, then without
|
||||
for _, keepOriginalHeader := range []bool{true, false} {
|
||||
patch := p.RenderPatchForFile(filename, true, reverseOnGenerate, keepOriginalHeader)
|
||||
if patch == "" {
|
||||
continue
|
||||
}
|
||||
if err = p.ApplyPatch(patch, applyFlags...); err != nil {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// clears the patch
|
||||
func (p *PatchManager) Reset() {
|
||||
p.CommitSha = ""
|
||||
p.fileInfoMap = map[string]*fileInfo{}
|
||||
}
|
||||
|
||||
func (p *PatchManager) CommitSelected() bool {
|
||||
return p.CommitSha != ""
|
||||
}
|
||||
|
||||
func (p *PatchManager) IsEmpty() bool {
|
||||
for _, fileInfo := range p.fileInfoMap {
|
||||
if fileInfo.mode == WHOLE || (fileInfo.mode == PART && len(fileInfo.includedLineIndices) > 0) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var hunkHeaderRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)[^\+]+\+(\d+)[^@]+@@(.*)$`)
|
||||
var patchHeaderRegexp = regexp.MustCompile(`(?ms)(^diff.*?)^@@`)
|
||||
|
||||
type PatchHunk struct {
|
||||
header string
|
||||
FirstLineIdx int
|
||||
LastLineIdx int
|
||||
bodyLines []string
|
||||
}
|
||||
|
||||
func newHunk(header string, body string, firstLineIdx int) *PatchHunk {
|
||||
bodyLines := strings.SplitAfter(header+body, "\n")[1:] // dropping the header line
|
||||
|
||||
return &PatchHunk{
|
||||
header: header,
|
||||
FirstLineIdx: firstLineIdx,
|
||||
LastLineIdx: firstLineIdx + len(bodyLines),
|
||||
bodyLines: bodyLines,
|
||||
}
|
||||
}
|
||||
|
||||
func (hunk *PatchHunk) updatedLines(lineIndices []int, reverse bool) []string {
|
||||
skippedNewlineMessageIndex := -1
|
||||
newLines := []string{}
|
||||
|
||||
lineIdx := hunk.FirstLineIdx
|
||||
for _, line := range hunk.bodyLines {
|
||||
lineIdx++ // incrementing at the start to skip the header line
|
||||
if line == "" {
|
||||
break
|
||||
}
|
||||
isLineSelected := utils.IncludesInt(lineIndices, lineIdx)
|
||||
|
||||
firstChar, content := line[:1], line[1:]
|
||||
transformedFirstChar := transformedFirstChar(firstChar, reverse, isLineSelected)
|
||||
|
||||
if isLineSelected || (transformedFirstChar == "\\" && skippedNewlineMessageIndex != lineIdx) || transformedFirstChar == " " {
|
||||
newLines = append(newLines, transformedFirstChar+content)
|
||||
continue
|
||||
}
|
||||
|
||||
if transformedFirstChar == "+" {
|
||||
// we don't want to include the 'newline at end of file' line if it involves an addition we're not including
|
||||
skippedNewlineMessageIndex = lineIdx + 1
|
||||
}
|
||||
}
|
||||
|
||||
return newLines
|
||||
}
|
||||
|
||||
func transformedFirstChar(firstChar string, reverse bool, isLineSelected bool) string {
|
||||
if reverse {
|
||||
if !isLineSelected && firstChar == "+" {
|
||||
return " "
|
||||
} else if firstChar == "-" {
|
||||
return "+"
|
||||
} else if firstChar == "+" {
|
||||
return "-"
|
||||
} else {
|
||||
return firstChar
|
||||
}
|
||||
}
|
||||
|
||||
if !isLineSelected && firstChar == "-" {
|
||||
return " "
|
||||
}
|
||||
|
||||
return firstChar
|
||||
}
|
||||
|
||||
func (hunk *PatchHunk) formatHeader(oldStart int, oldLength int, newStart int, newLength int, heading string) string {
|
||||
return fmt.Sprintf("@@ -%d,%d +%d,%d @@%s\n", oldStart, oldLength, newStart, newLength, heading)
|
||||
}
|
||||
|
||||
func (hunk *PatchHunk) formatWithChanges(lineIndices []int, reverse bool, startOffset int) (int, string) {
|
||||
bodyLines := hunk.updatedLines(lineIndices, reverse)
|
||||
startOffset, header, ok := hunk.updatedHeader(bodyLines, startOffset, reverse)
|
||||
if !ok {
|
||||
return startOffset, ""
|
||||
}
|
||||
return startOffset, header + strings.Join(bodyLines, "")
|
||||
}
|
||||
|
||||
func (hunk *PatchHunk) updatedHeader(newBodyLines []string, startOffset int, reverse bool) (int, string, bool) {
|
||||
changeCount := 0
|
||||
oldLength := 0
|
||||
newLength := 0
|
||||
for _, line := range newBodyLines {
|
||||
switch line[:1] {
|
||||
case "+":
|
||||
newLength++
|
||||
changeCount++
|
||||
case "-":
|
||||
oldLength++
|
||||
changeCount++
|
||||
case " ":
|
||||
oldLength++
|
||||
newLength++
|
||||
}
|
||||
}
|
||||
|
||||
if changeCount == 0 {
|
||||
// if nothing has changed we just return nothing
|
||||
return startOffset, "", false
|
||||
}
|
||||
|
||||
// get oldstart, newstart, and heading from header
|
||||
match := hunkHeaderRegexp.FindStringSubmatch(hunk.header)
|
||||
|
||||
var oldStart int
|
||||
if reverse {
|
||||
oldStart = mustConvertToInt(match[2])
|
||||
} else {
|
||||
oldStart = mustConvertToInt(match[1])
|
||||
}
|
||||
heading := match[3]
|
||||
|
||||
var newStartOffset int
|
||||
// if the hunk went from zero to positive length, we need to increment the starting point by one
|
||||
// if the hunk went from positive to zero length, we need to decrement the starting point by one
|
||||
if oldLength == 0 {
|
||||
newStartOffset = 1
|
||||
} else if newLength == 0 {
|
||||
newStartOffset = -1
|
||||
} else {
|
||||
newStartOffset = 0
|
||||
}
|
||||
|
||||
newStart := oldStart + startOffset + newStartOffset
|
||||
|
||||
newStartOffset = startOffset + newLength - oldLength
|
||||
formattedHeader := hunk.formatHeader(oldStart, oldLength, newStart, newLength, heading)
|
||||
return newStartOffset, formattedHeader, true
|
||||
}
|
||||
|
||||
func mustConvertToInt(s string) int {
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func GetHeaderFromDiff(diff string) string {
|
||||
match := patchHeaderRegexp.FindStringSubmatch(diff)
|
||||
if len(match) <= 1 {
|
||||
return ""
|
||||
}
|
||||
return match[1]
|
||||
}
|
||||
|
||||
func GetHunksFromDiff(diff string) []*PatchHunk {
|
||||
headers := hunkHeaderRegexp.FindAllString(diff, -1)
|
||||
bodies := hunkHeaderRegexp.Split(diff, -1)[1:] // discarding top bit
|
||||
|
||||
headerFirstLineIndices := []int{}
|
||||
for lineIdx, line := range strings.Split(diff, "\n") {
|
||||
if strings.HasPrefix(line, "@@ -") {
|
||||
headerFirstLineIndices = append(headerFirstLineIndices, lineIdx)
|
||||
}
|
||||
}
|
||||
|
||||
hunks := make([]*PatchHunk, len(headers))
|
||||
for index, header := range headers {
|
||||
hunks[index] = newHunk(header, bodies[index], headerFirstLineIndices[index])
|
||||
}
|
||||
|
||||
return hunks
|
||||
}
|
||||
|
||||
type PatchModifier struct {
|
||||
Log *logrus.Entry
|
||||
filename string
|
||||
hunks []*PatchHunk
|
||||
header string
|
||||
}
|
||||
|
||||
func NewPatchModifier(log *logrus.Entry, filename string, diffText string) *PatchModifier {
|
||||
return &PatchModifier{
|
||||
Log: log,
|
||||
filename: filename,
|
||||
hunks: GetHunksFromDiff(diffText),
|
||||
header: GetHeaderFromDiff(diffText),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *PatchModifier) ModifiedPatchForLines(lineIndices []int, reverse bool, keepOriginalHeader bool) string {
|
||||
// step one is getting only those hunks which we care about
|
||||
hunksInRange := []*PatchHunk{}
|
||||
outer:
|
||||
for _, hunk := range d.hunks {
|
||||
// if there is any line in our lineIndices array that the hunk contains, we append it
|
||||
for _, lineIdx := range lineIndices {
|
||||
if lineIdx >= hunk.FirstLineIdx && lineIdx <= hunk.LastLineIdx {
|
||||
hunksInRange = append(hunksInRange, hunk)
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// step 2 is collecting all the hunks with new headers
|
||||
startOffset := 0
|
||||
formattedHunks := ""
|
||||
var formattedHunk string
|
||||
for _, hunk := range hunksInRange {
|
||||
startOffset, formattedHunk = hunk.formatWithChanges(lineIndices, reverse, startOffset)
|
||||
formattedHunks += formattedHunk
|
||||
}
|
||||
|
||||
if formattedHunks == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
var fileHeader string
|
||||
// for staging/unstaging lines we don't want the original header because
|
||||
// it makes git confused e.g. when dealing with deleted/added files
|
||||
// but with building and applying patches the original header gives git
|
||||
// information it needs to cleanly apply patches
|
||||
if keepOriginalHeader {
|
||||
fileHeader = d.header
|
||||
} else {
|
||||
fileHeader = fmt.Sprintf("--- a/%s\n+++ b/%s\n", d.filename, d.filename)
|
||||
}
|
||||
|
||||
return fileHeader + formattedHunks
|
||||
}
|
||||
|
||||
func (d *PatchModifier) ModifiedPatchForRange(firstLineIdx int, lastLineIdx int, reverse bool, keepOriginalHeader bool) string {
|
||||
// generate array of consecutive line indices from our range
|
||||
selectedLines := []int{}
|
||||
for i := firstLineIdx; i <= lastLineIdx; i++ {
|
||||
selectedLines = append(selectedLines, i)
|
||||
}
|
||||
return d.ModifiedPatchForLines(selectedLines, reverse, keepOriginalHeader)
|
||||
}
|
||||
|
||||
func (d *PatchModifier) OriginalPatchLength() int {
|
||||
if len(d.hunks) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return d.hunks[len(d.hunks)-1].LastLineIdx
|
||||
}
|
||||
|
||||
func ModifiedPatchForRange(log *logrus.Entry, filename string, diffText string, firstLineIdx int, lastLineIdx int, reverse bool, keepOriginalHeader bool) string {
|
||||
p := NewPatchModifier(log, filename, diffText)
|
||||
return p.ModifiedPatchForRange(firstLineIdx, lastLineIdx, reverse, keepOriginalHeader)
|
||||
}
|
||||
@@ -0,0 +1,511 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const simpleDiff = `diff --git a/filename b/filename
|
||||
index dcd3485..1ba5540 100644
|
||||
--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,5 @@
|
||||
apple
|
||||
-orange
|
||||
+grape
|
||||
...
|
||||
...
|
||||
...
|
||||
`
|
||||
|
||||
const addNewlineToEndOfFile = `diff --git a/filename b/filename
|
||||
index 80a73f1..e48a11c 100644
|
||||
--- a/filename
|
||||
+++ b/filename
|
||||
@@ -60,4 +60,4 @@ grape
|
||||
...
|
||||
...
|
||||
...
|
||||
-last line
|
||||
\ No newline at end of file
|
||||
+last line
|
||||
`
|
||||
|
||||
const removeNewlinefromEndOfFile = `diff --git a/filename b/filename
|
||||
index e48a11c..80a73f1 100644
|
||||
--- a/filename
|
||||
+++ b/filename
|
||||
@@ -60,4 +60,4 @@ grape
|
||||
...
|
||||
...
|
||||
...
|
||||
-last line
|
||||
+last line
|
||||
\ No newline at end of file
|
||||
`
|
||||
|
||||
const twoHunks = `diff --git a/filename b/filename
|
||||
index e48a11c..b2ab81b 100644
|
||||
--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,5 @@
|
||||
apple
|
||||
-grape
|
||||
+orange
|
||||
...
|
||||
...
|
||||
...
|
||||
@@ -8,6 +8,8 @@ grape
|
||||
...
|
||||
...
|
||||
...
|
||||
+pear
|
||||
+lemon
|
||||
...
|
||||
...
|
||||
...
|
||||
`
|
||||
|
||||
const newFile = `diff --git a/newfile b/newfile
|
||||
new file mode 100644
|
||||
index 0000000..4e680cc
|
||||
--- /dev/null
|
||||
+++ b/newfile
|
||||
@@ -0,0 +1,3 @@
|
||||
+apple
|
||||
+orange
|
||||
+grape
|
||||
`
|
||||
|
||||
const addNewlineToPreviouslyEmptyFile = `diff --git a/newfile b/newfile
|
||||
index e69de29..c6568ea 100644
|
||||
--- a/newfile
|
||||
+++ b/newfile
|
||||
@@ -0,0 +1 @@
|
||||
+new line
|
||||
\ No newline at end of file
|
||||
`
|
||||
|
||||
// TestModifyPatchForRange is a function.
|
||||
func TestModifyPatchForRange(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
filename string
|
||||
diffText string
|
||||
firstLineIndex int
|
||||
lastLineIndex int
|
||||
reverse bool
|
||||
expected string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "nothing selected",
|
||||
filename: "filename",
|
||||
firstLineIndex: -1,
|
||||
lastLineIndex: -1,
|
||||
reverse: false,
|
||||
diffText: simpleDiff,
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
testName: "only context selected",
|
||||
filename: "filename",
|
||||
firstLineIndex: 5,
|
||||
lastLineIndex: 5,
|
||||
reverse: false,
|
||||
diffText: simpleDiff,
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
testName: "whole range selected",
|
||||
filename: "filename",
|
||||
firstLineIndex: 0,
|
||||
lastLineIndex: 11,
|
||||
reverse: false,
|
||||
diffText: simpleDiff,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,5 @@
|
||||
apple
|
||||
-orange
|
||||
+grape
|
||||
...
|
||||
...
|
||||
...
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "only removal selected",
|
||||
filename: "filename",
|
||||
firstLineIndex: 6,
|
||||
lastLineIndex: 6,
|
||||
reverse: false,
|
||||
diffText: simpleDiff,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,4 @@
|
||||
apple
|
||||
-orange
|
||||
...
|
||||
...
|
||||
...
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "only addition selected",
|
||||
filename: "filename",
|
||||
firstLineIndex: 7,
|
||||
lastLineIndex: 7,
|
||||
reverse: false,
|
||||
diffText: simpleDiff,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,6 @@
|
||||
apple
|
||||
orange
|
||||
+grape
|
||||
...
|
||||
...
|
||||
...
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "range that extends beyond diff bounds",
|
||||
filename: "filename",
|
||||
firstLineIndex: -100,
|
||||
lastLineIndex: 100,
|
||||
reverse: false,
|
||||
diffText: simpleDiff,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,5 @@
|
||||
apple
|
||||
-orange
|
||||
+grape
|
||||
...
|
||||
...
|
||||
...
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "whole range reversed",
|
||||
filename: "filename",
|
||||
firstLineIndex: 0,
|
||||
lastLineIndex: 11,
|
||||
reverse: true,
|
||||
diffText: simpleDiff,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,5 @@
|
||||
apple
|
||||
+orange
|
||||
-grape
|
||||
...
|
||||
...
|
||||
...
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "removal reversed",
|
||||
filename: "filename",
|
||||
firstLineIndex: 6,
|
||||
lastLineIndex: 6,
|
||||
reverse: true,
|
||||
diffText: simpleDiff,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,6 @@
|
||||
apple
|
||||
+orange
|
||||
grape
|
||||
...
|
||||
...
|
||||
...
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "removal reversed",
|
||||
filename: "filename",
|
||||
firstLineIndex: 7,
|
||||
lastLineIndex: 7,
|
||||
reverse: true,
|
||||
diffText: simpleDiff,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,4 @@
|
||||
apple
|
||||
-grape
|
||||
...
|
||||
...
|
||||
...
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "add newline to end of file",
|
||||
filename: "filename",
|
||||
firstLineIndex: -100,
|
||||
lastLineIndex: 100,
|
||||
reverse: false,
|
||||
diffText: addNewlineToEndOfFile,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -60,4 +60,4 @@ grape
|
||||
...
|
||||
...
|
||||
...
|
||||
-last line
|
||||
\ No newline at end of file
|
||||
+last line
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "add newline to end of file, addition only",
|
||||
filename: "filename",
|
||||
firstLineIndex: 8,
|
||||
lastLineIndex: 8,
|
||||
reverse: true,
|
||||
diffText: addNewlineToEndOfFile,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -60,4 +60,5 @@ grape
|
||||
...
|
||||
...
|
||||
...
|
||||
+last line
|
||||
\ No newline at end of file
|
||||
last line
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "add newline to end of file, removal only",
|
||||
filename: "filename",
|
||||
firstLineIndex: 10,
|
||||
lastLineIndex: 10,
|
||||
reverse: true,
|
||||
diffText: addNewlineToEndOfFile,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -60,4 +60,3 @@ grape
|
||||
...
|
||||
...
|
||||
...
|
||||
-last line
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "remove newline from end of file",
|
||||
filename: "filename",
|
||||
firstLineIndex: -100,
|
||||
lastLineIndex: 100,
|
||||
reverse: false,
|
||||
diffText: removeNewlinefromEndOfFile,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -60,4 +60,4 @@ grape
|
||||
...
|
||||
...
|
||||
...
|
||||
-last line
|
||||
+last line
|
||||
\ No newline at end of file
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "remove newline from end of file, removal only",
|
||||
filename: "filename",
|
||||
firstLineIndex: 8,
|
||||
lastLineIndex: 8,
|
||||
reverse: false,
|
||||
diffText: removeNewlinefromEndOfFile,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -60,4 +60,3 @@ grape
|
||||
...
|
||||
...
|
||||
...
|
||||
-last line
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "remove newline from end of file, addition only",
|
||||
filename: "filename",
|
||||
firstLineIndex: 9,
|
||||
lastLineIndex: 9,
|
||||
reverse: false,
|
||||
diffText: removeNewlinefromEndOfFile,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -60,4 +60,5 @@ grape
|
||||
...
|
||||
...
|
||||
...
|
||||
last line
|
||||
+last line
|
||||
\ No newline at end of file
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "staging two whole hunks",
|
||||
filename: "filename",
|
||||
firstLineIndex: -100,
|
||||
lastLineIndex: 100,
|
||||
reverse: false,
|
||||
diffText: twoHunks,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,5 @@
|
||||
apple
|
||||
-grape
|
||||
+orange
|
||||
...
|
||||
...
|
||||
...
|
||||
@@ -8,6 +8,8 @@ grape
|
||||
...
|
||||
...
|
||||
...
|
||||
+pear
|
||||
+lemon
|
||||
...
|
||||
...
|
||||
...
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "staging part of both hunks",
|
||||
filename: "filename",
|
||||
firstLineIndex: 7,
|
||||
lastLineIndex: 15,
|
||||
reverse: false,
|
||||
diffText: twoHunks,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,6 @@
|
||||
apple
|
||||
grape
|
||||
+orange
|
||||
...
|
||||
...
|
||||
...
|
||||
@@ -8,6 +9,7 @@ grape
|
||||
...
|
||||
...
|
||||
...
|
||||
+pear
|
||||
...
|
||||
...
|
||||
...
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "staging part of both hunks, reversed",
|
||||
filename: "filename",
|
||||
firstLineIndex: 7,
|
||||
lastLineIndex: 15,
|
||||
reverse: true,
|
||||
diffText: twoHunks,
|
||||
expected: `--- a/filename
|
||||
+++ b/filename
|
||||
@@ -1,5 +1,4 @@
|
||||
apple
|
||||
-orange
|
||||
...
|
||||
...
|
||||
...
|
||||
@@ -8,8 +7,7 @@ grape
|
||||
...
|
||||
...
|
||||
...
|
||||
-pear
|
||||
lemon
|
||||
...
|
||||
...
|
||||
...
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "adding a new file",
|
||||
filename: "newfile",
|
||||
firstLineIndex: -100,
|
||||
lastLineIndex: 100,
|
||||
reverse: false,
|
||||
diffText: newFile,
|
||||
expected: `--- a/newfile
|
||||
+++ b/newfile
|
||||
@@ -0,0 +1,3 @@
|
||||
+apple
|
||||
+orange
|
||||
+grape
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "adding part of a new file",
|
||||
filename: "newfile",
|
||||
firstLineIndex: 6,
|
||||
lastLineIndex: 7,
|
||||
reverse: false,
|
||||
diffText: newFile,
|
||||
expected: `--- a/newfile
|
||||
+++ b/newfile
|
||||
@@ -0,0 +1,2 @@
|
||||
+apple
|
||||
+orange
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "adding a new file, reversed",
|
||||
filename: "newfile",
|
||||
firstLineIndex: -100,
|
||||
lastLineIndex: 100,
|
||||
reverse: true,
|
||||
diffText: newFile,
|
||||
expected: `--- a/newfile
|
||||
+++ b/newfile
|
||||
@@ -1,3 +0,0 @@
|
||||
-apple
|
||||
-orange
|
||||
-grape
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "adding a new line to a previously empty file",
|
||||
filename: "newfile",
|
||||
firstLineIndex: -100,
|
||||
lastLineIndex: 100,
|
||||
reverse: false,
|
||||
diffText: addNewlineToPreviouslyEmptyFile,
|
||||
expected: `--- a/newfile
|
||||
+++ b/newfile
|
||||
@@ -0,0 +1,1 @@
|
||||
+new line
|
||||
\ No newline at end of file
|
||||
`,
|
||||
},
|
||||
{
|
||||
testName: "adding a new line to a previously empty file, reversed",
|
||||
filename: "newfile",
|
||||
firstLineIndex: -100,
|
||||
lastLineIndex: 100,
|
||||
reverse: true,
|
||||
diffText: addNewlineToPreviouslyEmptyFile,
|
||||
expected: `--- a/newfile
|
||||
+++ b/newfile
|
||||
@@ -1,1 +0,0 @@
|
||||
-new line
|
||||
\ No newline at end of file
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
result := ModifiedPatchForRange(nil, s.filename, s.diffText, s.firstLineIndex, s.lastLineIndex, s.reverse, false)
|
||||
if !assert.Equal(t, s.expected, result) {
|
||||
fmt.Println(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
PATCH_HEADER = iota
|
||||
COMMIT_SHA
|
||||
COMMIT_DESCRIPTION
|
||||
HUNK_HEADER
|
||||
ADDITION
|
||||
DELETION
|
||||
CONTEXT
|
||||
NEWLINE_MESSAGE
|
||||
)
|
||||
|
||||
// the job of this file is to parse a diff, find out where the hunks begin and end, which lines are stageable, and how to find the next hunk from the current position or the next stageable line from the current position.
|
||||
|
||||
type PatchLine struct {
|
||||
Kind int
|
||||
Content string // something like '+ hello' (note the first character is not removed)
|
||||
}
|
||||
|
||||
type PatchParser struct {
|
||||
Log *logrus.Entry
|
||||
PatchLines []*PatchLine
|
||||
PatchHunks []*PatchHunk
|
||||
HunkStarts []int
|
||||
StageableLines []int // rename to mention we're talking about indexes
|
||||
}
|
||||
|
||||
// NewPatchParser builds a new branch list builder
|
||||
func NewPatchParser(log *logrus.Entry, patch string) (*PatchParser, error) {
|
||||
hunkStarts, stageableLines, patchLines, err := parsePatch(patch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
patchHunks := GetHunksFromDiff(patch)
|
||||
|
||||
return &PatchParser{
|
||||
Log: log,
|
||||
HunkStarts: hunkStarts, // deprecated
|
||||
StageableLines: stageableLines,
|
||||
PatchLines: patchLines,
|
||||
PatchHunks: patchHunks,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetHunkContainingLine takes a line index and an offset and finds the hunk
|
||||
// which contains the line index, then returns the hunk considering the offset.
|
||||
// e.g. if the offset is 1 it will return the next hunk.
|
||||
func (p *PatchParser) GetHunkContainingLine(lineIndex int, offset int) *PatchHunk {
|
||||
if len(p.PatchHunks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for index, hunk := range p.PatchHunks {
|
||||
if lineIndex >= hunk.FirstLineIdx && lineIndex <= hunk.LastLineIdx {
|
||||
resultIndex := index + offset
|
||||
if resultIndex < 0 {
|
||||
resultIndex = 0
|
||||
} else if resultIndex > len(p.PatchHunks)-1 {
|
||||
resultIndex = len(p.PatchHunks) - 1
|
||||
}
|
||||
return p.PatchHunks[resultIndex]
|
||||
}
|
||||
}
|
||||
|
||||
// if your cursor is past the last hunk, select the last hunk
|
||||
if lineIndex > p.PatchHunks[len(p.PatchHunks)-1].LastLineIdx {
|
||||
return p.PatchHunks[len(p.PatchHunks)-1]
|
||||
}
|
||||
|
||||
// otherwise select the first
|
||||
return p.PatchHunks[0]
|
||||
}
|
||||
|
||||
// selected means you've got it highlighted with your cursor
|
||||
// included means the line has been included in the patch (only applicable when
|
||||
// building a patch)
|
||||
func (l *PatchLine) render(selected bool, included bool) string {
|
||||
content := l.Content
|
||||
if len(content) == 0 {
|
||||
content = " " // using the space so that we can still highlight if necessary
|
||||
}
|
||||
|
||||
// for hunk headers we need to start off cyan and then use white for the message
|
||||
if l.Kind == HUNK_HEADER {
|
||||
re := regexp.MustCompile("(@@.*?@@)(.*)")
|
||||
match := re.FindStringSubmatch(content)
|
||||
return coloredString(color.FgCyan, match[1], selected, included) + coloredString(theme.DefaultTextColor, match[2], selected, false)
|
||||
}
|
||||
|
||||
var colorAttr color.Attribute
|
||||
switch l.Kind {
|
||||
case PATCH_HEADER:
|
||||
colorAttr = color.Bold
|
||||
case ADDITION:
|
||||
colorAttr = color.FgGreen
|
||||
case DELETION:
|
||||
colorAttr = color.FgRed
|
||||
case COMMIT_SHA:
|
||||
colorAttr = color.FgYellow
|
||||
default:
|
||||
colorAttr = theme.DefaultTextColor
|
||||
}
|
||||
|
||||
return coloredString(colorAttr, content, selected, included)
|
||||
}
|
||||
|
||||
func coloredString(colorAttr color.Attribute, str string, selected bool, included bool) string {
|
||||
var cl *color.Color
|
||||
attributes := []color.Attribute{colorAttr}
|
||||
if selected {
|
||||
attributes = append(attributes, color.BgBlue)
|
||||
}
|
||||
cl = color.New(attributes...)
|
||||
var clIncluded *color.Color
|
||||
if included {
|
||||
clIncluded = color.New(append(attributes, color.BgGreen)...)
|
||||
} else {
|
||||
clIncluded = color.New(attributes...)
|
||||
}
|
||||
|
||||
if len(str) < 2 {
|
||||
return utils.ColoredStringDirect(str, clIncluded)
|
||||
}
|
||||
|
||||
return utils.ColoredStringDirect(str[:1], clIncluded) + utils.ColoredStringDirect(str[1:], cl)
|
||||
}
|
||||
|
||||
func parsePatch(patch string) ([]int, []int, []*PatchLine, error) {
|
||||
lines := strings.Split(patch, "\n")
|
||||
hunkStarts := []int{}
|
||||
stageableLines := []int{}
|
||||
pastFirstHunkHeader := false
|
||||
pastCommitDescription := true
|
||||
patchLines := make([]*PatchLine, len(lines))
|
||||
var lineKind int
|
||||
var firstChar string
|
||||
for index, line := range lines {
|
||||
firstChar = " "
|
||||
if len(line) > 0 {
|
||||
firstChar = line[:1]
|
||||
}
|
||||
if index == 0 && strings.HasPrefix(line, "commit") {
|
||||
lineKind = COMMIT_SHA
|
||||
pastCommitDescription = false
|
||||
} else if !pastCommitDescription {
|
||||
if strings.HasPrefix(line, "diff") || strings.HasPrefix(line, "---") {
|
||||
pastCommitDescription = true
|
||||
lineKind = PATCH_HEADER
|
||||
} else {
|
||||
lineKind = COMMIT_DESCRIPTION
|
||||
}
|
||||
} else if firstChar == "@" {
|
||||
pastFirstHunkHeader = true
|
||||
hunkStarts = append(hunkStarts, index)
|
||||
lineKind = HUNK_HEADER
|
||||
} else if pastFirstHunkHeader {
|
||||
switch firstChar {
|
||||
case "-":
|
||||
lineKind = DELETION
|
||||
stageableLines = append(stageableLines, index)
|
||||
case "+":
|
||||
lineKind = ADDITION
|
||||
stageableLines = append(stageableLines, index)
|
||||
case "\\":
|
||||
lineKind = NEWLINE_MESSAGE
|
||||
case " ":
|
||||
lineKind = CONTEXT
|
||||
}
|
||||
} else {
|
||||
lineKind = PATCH_HEADER
|
||||
}
|
||||
patchLines[index] = &PatchLine{Kind: lineKind, Content: line}
|
||||
}
|
||||
return hunkStarts, stageableLines, patchLines, nil
|
||||
}
|
||||
|
||||
// Render returns the coloured string of the diff with any selected lines highlighted
|
||||
func (p *PatchParser) Render(firstLineIndex int, lastLineIndex int, incLineIndices []int) string {
|
||||
renderedLines := make([]string, len(p.PatchLines))
|
||||
for index, patchLine := range p.PatchLines {
|
||||
selected := index >= firstLineIndex && index <= lastLineIndex
|
||||
included := utils.IncludesInt(incLineIndices, index)
|
||||
renderedLines[index] = patchLine.render(selected, included)
|
||||
}
|
||||
result := strings.Join(renderedLines, "\n")
|
||||
if strings.TrimSpace(utils.Decolorise(result)) == "" {
|
||||
return ""
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetNextStageableLineIndex takes a line index and returns the line index of the next stageable line
|
||||
// note this will actually include the current index if it is stageable
|
||||
func (p *PatchParser) GetNextStageableLineIndex(currentIndex int) int {
|
||||
for _, lineIndex := range p.StageableLines {
|
||||
if lineIndex >= currentIndex {
|
||||
return lineIndex
|
||||
}
|
||||
}
|
||||
return p.StageableLines[len(p.StageableLines)-1]
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package commands
|
||||
|
||||
import "github.com/go-errors/errors"
|
||||
|
||||
// DeletePatchesFromCommit applies a patch in reverse for a commit
|
||||
func (c *GitCommand) DeletePatchesFromCommit(commits []*Commit, commitIndex int, p *PatchManager) error {
|
||||
if err := c.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// apply each patch in reverse
|
||||
if err := p.ApplyPatches(true); err != nil {
|
||||
if err := c.GenericMerge("rebase", "abort"); err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// time to amend the selected commit
|
||||
if _, err := c.AmendHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.onSuccessfulContinue = func() error {
|
||||
c.PatchManager.Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
// continue
|
||||
return c.GenericMerge("rebase", "continue")
|
||||
}
|
||||
|
||||
func (c *GitCommand) MovePatchToSelectedCommit(commits []*Commit, sourceCommitIdx int, destinationCommitIdx int, p *PatchManager) error {
|
||||
if sourceCommitIdx < destinationCommitIdx {
|
||||
if err := c.BeginInteractiveRebaseForCommit(commits, destinationCommitIdx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// apply each patch forward
|
||||
if err := p.ApplyPatches(false); err != nil {
|
||||
if err := c.GenericMerge("rebase", "abort"); err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// amend the destination commit
|
||||
if _, err := c.AmendHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.onSuccessfulContinue = func() error {
|
||||
c.PatchManager.Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
// continue
|
||||
return c.GenericMerge("rebase", "continue")
|
||||
}
|
||||
|
||||
if len(commits)-1 < sourceCommitIdx {
|
||||
return errors.New("index outside of range of commits")
|
||||
}
|
||||
|
||||
// we can make this GPG thing possible it just means we need to do this in two parts:
|
||||
// one where we handle the possibility of a credential request, and the other
|
||||
// where we continue the rebase
|
||||
if c.usingGpg() {
|
||||
return errors.New(c.Tr.SLocalize("DisabledForGPG"))
|
||||
}
|
||||
|
||||
baseIndex := sourceCommitIdx + 1
|
||||
todo := ""
|
||||
for i, commit := range commits[0:baseIndex] {
|
||||
a := "pick"
|
||||
if i == sourceCommitIdx || i == destinationCommitIdx {
|
||||
a = "edit"
|
||||
}
|
||||
todo = a + " " + commit.Sha + " " + commit.Name + "\n" + todo
|
||||
}
|
||||
|
||||
cmd, err := c.PrepareInteractiveRebaseCommand(commits[baseIndex].Sha, todo, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.OSCommand.RunPreparedCommand(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// apply each patch in reverse
|
||||
if err := p.ApplyPatches(true); err != nil {
|
||||
if err := c.GenericMerge("rebase", "abort"); err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// amend the source commit
|
||||
if _, err := c.AmendHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.onSuccessfulContinue != nil {
|
||||
return errors.New("You are midway through another rebase operation. Please abort to start again")
|
||||
}
|
||||
|
||||
c.onSuccessfulContinue = func() error {
|
||||
// now we should be up to the destination, so let's apply forward these patches to that.
|
||||
// ideally we would ensure we're on the right commit but I'm not sure if that check is necessary
|
||||
if err := p.ApplyPatches(false); err != nil {
|
||||
if err := c.GenericMerge("rebase", "abort"); err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// amend the destination commit
|
||||
if _, err := c.AmendHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.onSuccessfulContinue = func() error {
|
||||
c.PatchManager.Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.GenericMerge("rebase", "continue")
|
||||
}
|
||||
|
||||
return c.GenericMerge("rebase", "continue")
|
||||
}
|
||||
|
||||
func (c *GitCommand) PullPatchIntoIndex(commits []*Commit, commitIdx int, p *PatchManager) error {
|
||||
if err := c.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.ApplyPatches(true); err != nil {
|
||||
if err := c.GenericMerge("rebase", "abort"); err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// amend the commit
|
||||
if _, err := c.AmendHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.onSuccessfulContinue != nil {
|
||||
return errors.New("You are midway through another rebase operation. Please abort to start again")
|
||||
}
|
||||
|
||||
c.onSuccessfulContinue = func() error {
|
||||
// add patches to index
|
||||
if err := p.ApplyPatches(false); err != nil {
|
||||
if err := c.GenericMerge("rebase", "abort"); err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
c.PatchManager.Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.GenericMerge("rebase", "continue")
|
||||
}
|
||||
@@ -34,7 +34,7 @@ func getServices() []*Service {
|
||||
},
|
||||
{
|
||||
Name: "bitbucket.org",
|
||||
PullRequestURL: "https://bitbucket.org/%s/%s/pull-requests/new?t=%s",
|
||||
PullRequestURL: "https://bitbucket.org/%s/%s/pull-requests/new?source=%s&t=1",
|
||||
},
|
||||
{
|
||||
Name: "gitlab.com",
|
||||
|
||||
@@ -64,7 +64,7 @@ func TestCreatePullRequest(t *testing.T) {
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "open")
|
||||
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?t=feature/profile-page"})
|
||||
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1"})
|
||||
return exec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
@@ -83,7 +83,7 @@ func TestCreatePullRequest(t *testing.T) {
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "open")
|
||||
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?t=feature/events"})
|
||||
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1"})
|
||||
return exec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
|
||||
@@ -7,7 +7,7 @@ type StashEntry struct {
|
||||
DisplayString string
|
||||
}
|
||||
|
||||
// GetDisplayStrings returns the dispaly string of branch
|
||||
// GetDisplayStrings returns the display string of branch
|
||||
func (s *StashEntry) GetDisplayStrings(isFocused bool) []string {
|
||||
return []string{s.DisplayString}
|
||||
}
|
||||
|
||||
Vendored
+49
-37
@@ -13,15 +13,16 @@ import (
|
||||
|
||||
// AppConfig contains the base configuration fields required for lazygit.
|
||||
type AppConfig struct {
|
||||
Debug bool `long:"debug" env:"DEBUG" default:"false"`
|
||||
Version string `long:"version" env:"VERSION" default:"unversioned"`
|
||||
Commit string `long:"commit" env:"COMMIT"`
|
||||
BuildDate string `long:"build-date" env:"BUILD_DATE"`
|
||||
Name string `long:"name" env:"NAME" default:"lazygit"`
|
||||
BuildSource string `long:"build-source" env:"BUILD_SOURCE" default:""`
|
||||
UserConfig *viper.Viper
|
||||
AppState *AppState
|
||||
IsNewRepo bool
|
||||
Debug bool `long:"debug" env:"DEBUG" default:"false"`
|
||||
Version string `long:"version" env:"VERSION" default:"unversioned"`
|
||||
Commit string `long:"commit" env:"COMMIT"`
|
||||
BuildDate string `long:"build-date" env:"BUILD_DATE"`
|
||||
Name string `long:"name" env:"NAME" default:"lazygit"`
|
||||
BuildSource string `long:"build-source" env:"BUILD_SOURCE" default:""`
|
||||
UserConfig *viper.Viper
|
||||
UserConfigDir string
|
||||
AppState *AppState
|
||||
IsNewRepo bool
|
||||
}
|
||||
|
||||
// AppConfigurer interface allows individual app config structs to inherit Fields
|
||||
@@ -34,8 +35,9 @@ type AppConfigurer interface {
|
||||
GetName() string
|
||||
GetBuildSource() string
|
||||
GetUserConfig() *viper.Viper
|
||||
GetUserConfigDir() string
|
||||
GetAppState() *AppState
|
||||
WriteToUserConfig(string, string) error
|
||||
WriteToUserConfig(string, interface{}) error
|
||||
SaveAppState() error
|
||||
LoadAppState() error
|
||||
SetIsNewRepo(bool)
|
||||
@@ -44,7 +46,7 @@ type AppConfigurer interface {
|
||||
|
||||
// NewAppConfig makes a new app config
|
||||
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag bool) (*AppConfig, error) {
|
||||
userConfig, err := LoadConfig("config", true)
|
||||
userConfig, userConfigPath, err := LoadConfig("config", true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -54,15 +56,16 @@ func NewAppConfig(name, version, commit, date string, buildSource string, debugg
|
||||
}
|
||||
|
||||
appConfig := &AppConfig{
|
||||
Name: "lazygit",
|
||||
Version: version,
|
||||
Commit: commit,
|
||||
BuildDate: date,
|
||||
Debug: debuggingFlag,
|
||||
BuildSource: buildSource,
|
||||
UserConfig: userConfig,
|
||||
AppState: &AppState{},
|
||||
IsNewRepo: false,
|
||||
Name: "lazygit",
|
||||
Version: version,
|
||||
Commit: commit,
|
||||
BuildDate: date,
|
||||
Debug: debuggingFlag,
|
||||
BuildSource: buildSource,
|
||||
UserConfig: userConfig,
|
||||
UserConfigDir: filepath.Dir(userConfigPath),
|
||||
AppState: &AppState{},
|
||||
IsNewRepo: false,
|
||||
}
|
||||
|
||||
if err := appConfig.LoadAppState(); err != nil {
|
||||
@@ -123,6 +126,10 @@ func (c *AppConfig) GetAppState() *AppState {
|
||||
return c.AppState
|
||||
}
|
||||
|
||||
func (c *AppConfig) GetUserConfigDir() string {
|
||||
return c.UserConfigDir
|
||||
}
|
||||
|
||||
func newViper(filename string) (*viper.Viper, error) {
|
||||
v := viper.New()
|
||||
v.SetConfigType("yaml")
|
||||
@@ -131,23 +138,24 @@ func newViper(filename string) (*viper.Viper, error) {
|
||||
}
|
||||
|
||||
// LoadConfig gets the user's config
|
||||
func LoadConfig(filename string, withDefaults bool) (*viper.Viper, error) {
|
||||
func LoadConfig(filename string, withDefaults bool) (*viper.Viper, string, error) {
|
||||
v, err := newViper(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
if withDefaults {
|
||||
if err = LoadDefaults(v, GetDefaultConfig()); err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
if err = LoadDefaults(v, GetPlatformDefaultConfig()); err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
}
|
||||
if err = LoadAndMergeFile(v, filename+".yml"); err != nil {
|
||||
return nil, err
|
||||
configPath, err := LoadAndMergeFile(v, filename+".yml")
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return v, nil
|
||||
return v, configPath, nil
|
||||
}
|
||||
|
||||
// LoadDefaults loads in the defaults defined in this file
|
||||
@@ -173,21 +181,21 @@ func prepareConfigFile(filename string) (string, error) {
|
||||
|
||||
// LoadAndMergeFile Loads the config/state file, creating
|
||||
// the file has an empty one if it does not exist
|
||||
func LoadAndMergeFile(v *viper.Viper, filename string) error {
|
||||
func LoadAndMergeFile(v *viper.Viper, filename string) (string, error) {
|
||||
configPath, err := prepareConfigFile(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
v.AddConfigPath(filepath.Dir(configPath))
|
||||
return v.MergeInConfig()
|
||||
return configPath, v.MergeInConfig()
|
||||
}
|
||||
|
||||
// WriteToUserConfig adds a key/value pair to the user's config and saves it
|
||||
func (c *AppConfig) WriteToUserConfig(key, value string) error {
|
||||
func (c *AppConfig) WriteToUserConfig(key string, value interface{}) error {
|
||||
// reloading the user config directly (without defaults) so that we're not
|
||||
// writing any defaults back to the user's config
|
||||
v, err := LoadConfig("config", false)
|
||||
v, _, err := LoadConfig("config", false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -196,7 +204,7 @@ func (c *AppConfig) WriteToUserConfig(key, value string) error {
|
||||
return v.WriteConfig()
|
||||
}
|
||||
|
||||
// SaveAppState marhsalls the AppState struct and writes it to the disk
|
||||
// SaveAppState marshalls the AppState struct and writes it to the disk
|
||||
func (c *AppConfig) SaveAppState() error {
|
||||
marshalledAppState, err := yaml.Marshal(c.AppState)
|
||||
if err != nil {
|
||||
@@ -234,8 +242,9 @@ func GetDefaultConfig() []byte {
|
||||
## stuff relating to the UI
|
||||
scrollHeight: 2
|
||||
scrollPastBottom: true
|
||||
mouseEvents: false # will default to true when the feature is complete
|
||||
mouseEvents: true
|
||||
theme:
|
||||
lightTheme: false
|
||||
activeBorderColor:
|
||||
- white
|
||||
- bold
|
||||
@@ -245,13 +254,16 @@ func GetDefaultConfig() []byte {
|
||||
- blue
|
||||
commitLength:
|
||||
show: true
|
||||
git:
|
||||
merging:
|
||||
manualCommit: false
|
||||
git:
|
||||
merging:
|
||||
manualCommit: false
|
||||
skipHookPrefix: 'WIP'
|
||||
autoFetch: true
|
||||
update:
|
||||
method: prompt # can be: prompt | background | never
|
||||
days: 14 # how often a update is checked for
|
||||
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
|
||||
splashUpdatesIndex: 0
|
||||
confirmOnQuit: false
|
||||
`)
|
||||
}
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type PatchModifier struct {
|
||||
Log *logrus.Entry
|
||||
Tr *i18n.Localizer
|
||||
}
|
||||
|
||||
// NewPatchModifier builds a new branch list builder
|
||||
func NewPatchModifier(log *logrus.Entry) (*PatchModifier, error) {
|
||||
return &PatchModifier{
|
||||
Log: log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ModifyPatchForHunk takes the original patch, which may contain several hunks,
|
||||
// and removes any hunks that aren't the selected hunk
|
||||
func (p *PatchModifier) ModifyPatchForHunk(patch string, hunkStarts []int, currentLine int) (string, error) {
|
||||
// get hunk start and end
|
||||
lines := strings.Split(patch, "\n")
|
||||
hunkStartIndex := utils.PrevIndex(hunkStarts, currentLine)
|
||||
hunkStart := hunkStarts[hunkStartIndex]
|
||||
nextHunkStartIndex := utils.NextIndex(hunkStarts, currentLine)
|
||||
var hunkEnd int
|
||||
if nextHunkStartIndex == 0 {
|
||||
hunkEnd = len(lines) - 1
|
||||
} else {
|
||||
hunkEnd = hunkStarts[nextHunkStartIndex]
|
||||
}
|
||||
|
||||
headerLength, err := p.getHeaderLength(lines)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
output := strings.Join(lines[0:headerLength], "\n") + "\n"
|
||||
output += strings.Join(lines[hunkStart:hunkEnd], "\n") + "\n"
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (p *PatchModifier) getHeaderLength(patchLines []string) (int, error) {
|
||||
for index, line := range patchLines {
|
||||
if strings.HasPrefix(line, "@@") {
|
||||
return index, nil
|
||||
}
|
||||
}
|
||||
return 0, errors.New(p.Tr.SLocalize("CantFindHunks"))
|
||||
}
|
||||
|
||||
// ModifyPatchForLine takes the original patch, which may contain several hunks,
|
||||
// and the line number of the line we want to stage
|
||||
func (p *PatchModifier) ModifyPatchForLine(patch string, lineNumber int) (string, error) {
|
||||
lines := strings.Split(patch, "\n")
|
||||
headerLength, err := p.getHeaderLength(lines)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
output := strings.Join(lines[0:headerLength], "\n") + "\n"
|
||||
|
||||
hunkStart, err := p.getHunkStart(lines, lineNumber)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hunk, err := p.getModifiedHunk(lines, hunkStart, lineNumber)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
output += strings.Join(hunk, "\n")
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// getHunkStart returns the line number of the hunk we're going to be modifying
|
||||
// in order to stage our line
|
||||
func (p *PatchModifier) getHunkStart(patchLines []string, lineNumber int) (int, error) {
|
||||
// find the hunk that we're modifying
|
||||
hunkStart := 0
|
||||
for index, line := range patchLines {
|
||||
if strings.HasPrefix(line, "@@") {
|
||||
hunkStart = index
|
||||
}
|
||||
if index == lineNumber {
|
||||
return hunkStart, nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0, errors.New(p.Tr.SLocalize("CantFindHunk"))
|
||||
}
|
||||
|
||||
func (p *PatchModifier) getModifiedHunk(patchLines []string, hunkStart int, lineNumber int) ([]string, error) {
|
||||
lineChanges := 0
|
||||
// strip the hunk down to just the line we want to stage
|
||||
newHunk := []string{patchLines[hunkStart]}
|
||||
for offsetIndex, line := range patchLines[hunkStart+1:] {
|
||||
index := offsetIndex + hunkStart + 1
|
||||
if strings.HasPrefix(line, "@@") {
|
||||
newHunk = append(newHunk, "\n")
|
||||
break
|
||||
}
|
||||
if index != lineNumber {
|
||||
// we include other removals but treat them like context
|
||||
if strings.HasPrefix(line, "-") {
|
||||
newHunk = append(newHunk, " "+line[1:])
|
||||
lineChanges += 1
|
||||
continue
|
||||
}
|
||||
// we don't include other additions
|
||||
if strings.HasPrefix(line, "+") {
|
||||
lineChanges -= 1
|
||||
continue
|
||||
}
|
||||
}
|
||||
newHunk = append(newHunk, line)
|
||||
}
|
||||
|
||||
var err error
|
||||
newHunk[0], err = p.updatedHeader(newHunk[0], lineChanges)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newHunk, nil
|
||||
}
|
||||
|
||||
// updatedHeader returns the hunk header with the updated line range
|
||||
// we need to update the hunk length to reflect the changes we made
|
||||
// if the hunk has three additions but we're only staging one, then
|
||||
// @@ -14,8 +14,11 @@ import (
|
||||
// becomes
|
||||
// @@ -14,8 +14,9 @@ import (
|
||||
func (p *PatchModifier) updatedHeader(currentHeader string, lineChanges int) (string, error) {
|
||||
// current counter is the number after the second comma
|
||||
re := regexp.MustCompile(`(\d+) @@`)
|
||||
prevLengthString := re.FindStringSubmatch(currentHeader)[1]
|
||||
|
||||
prevLength, err := strconv.Atoi(prevLengthString)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
re = regexp.MustCompile(`\d+ @@`)
|
||||
newLength := strconv.Itoa(prevLength + lineChanges)
|
||||
return re.ReplaceAllString(currentHeader, newLength+" @@"), nil
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// NewDummyPatchModifier constructs a new dummy patch modifier for testing
|
||||
func NewDummyPatchModifier() *PatchModifier {
|
||||
return &PatchModifier{
|
||||
Log: commands.NewDummyLog(),
|
||||
}
|
||||
}
|
||||
|
||||
func TestModifyPatchForLine(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
patchFilename string
|
||||
lineNumber int
|
||||
shouldError bool
|
||||
expectedPatchFilename string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Removing one line",
|
||||
"testdata/testPatchBefore.diff",
|
||||
8,
|
||||
false,
|
||||
"testdata/testPatchAfter1.diff",
|
||||
},
|
||||
{
|
||||
"Adding one line",
|
||||
"testdata/testPatchBefore.diff",
|
||||
10,
|
||||
false,
|
||||
"testdata/testPatchAfter2.diff",
|
||||
},
|
||||
{
|
||||
"Adding one line in top hunk in diff with multiple hunks",
|
||||
"testdata/testPatchBefore2.diff",
|
||||
20,
|
||||
false,
|
||||
"testdata/testPatchAfter3.diff",
|
||||
},
|
||||
{
|
||||
"Adding one line in top hunk in diff with multiple hunks",
|
||||
"testdata/testPatchBefore2.diff",
|
||||
53,
|
||||
false,
|
||||
"testdata/testPatchAfter4.diff",
|
||||
},
|
||||
{
|
||||
"adding unstaged file with a single line",
|
||||
"testdata/addedFile.diff",
|
||||
6,
|
||||
false,
|
||||
"testdata/addedFile.diff",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
p := NewDummyPatchModifier()
|
||||
beforePatch, err := ioutil.ReadFile(s.patchFilename)
|
||||
if err != nil {
|
||||
panic("Cannot open file at " + s.patchFilename)
|
||||
}
|
||||
afterPatch, err := p.ModifyPatchForLine(string(beforePatch), s.lineNumber)
|
||||
if s.shouldError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
expected, err := ioutil.ReadFile(s.expectedPatchFilename)
|
||||
if err != nil {
|
||||
panic("Cannot open file at " + s.expectedPatchFilename)
|
||||
}
|
||||
assert.Equal(t, string(expected), afterPatch)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type PatchParser struct {
|
||||
Log *logrus.Entry
|
||||
}
|
||||
|
||||
// NewPatchParser builds a new branch list builder
|
||||
func NewPatchParser(log *logrus.Entry) (*PatchParser, error) {
|
||||
return &PatchParser{
|
||||
Log: log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *PatchParser) ParsePatch(patch string) ([]int, []int, error) {
|
||||
lines := strings.Split(patch, "\n")
|
||||
hunkStarts := []int{}
|
||||
stageableLines := []int{}
|
||||
pastHeader := false
|
||||
for index, line := range lines {
|
||||
if strings.HasPrefix(line, "@@") {
|
||||
pastHeader = true
|
||||
hunkStarts = append(hunkStarts, index)
|
||||
}
|
||||
if pastHeader && (strings.HasPrefix(line, "-") || strings.HasPrefix(line, "+")) {
|
||||
stageableLines = append(stageableLines, index)
|
||||
}
|
||||
}
|
||||
p.Log.WithField("staging", "staging").Info(stageableLines)
|
||||
return hunkStarts, stageableLines, nil
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// NewDummyPatchParser constructs a new dummy patch parser for testing
|
||||
func NewDummyPatchParser() *PatchParser {
|
||||
return &PatchParser{
|
||||
Log: commands.NewDummyLog(),
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePatch(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
patchFilename string
|
||||
shouldError bool
|
||||
expectedStageableLines []int
|
||||
expectedHunkStarts []int
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Diff with one hunk",
|
||||
"testdata/testPatchBefore.diff",
|
||||
false,
|
||||
[]int{8, 9, 10, 11},
|
||||
[]int{4},
|
||||
},
|
||||
{
|
||||
"Diff with two hunks",
|
||||
"testdata/testPatchBefore2.diff",
|
||||
false,
|
||||
[]int{8, 9, 10, 11, 12, 13, 20, 21, 22, 23, 24, 25, 26, 27, 28, 33, 34, 35, 36, 37, 45, 46, 47, 48, 49, 50, 51, 52, 53},
|
||||
[]int{4, 41},
|
||||
},
|
||||
{
|
||||
"Unstaged file",
|
||||
"testdata/addedFile.diff",
|
||||
false,
|
||||
[]int{6},
|
||||
[]int{5},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
p := NewDummyPatchParser()
|
||||
beforePatch, err := ioutil.ReadFile(s.patchFilename)
|
||||
if err != nil {
|
||||
panic("Cannot open file at " + s.patchFilename)
|
||||
}
|
||||
hunkStarts, stageableLines, err := p.ParsePatch(string(beforePatch))
|
||||
if s.shouldError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, s.expectedStageableLines, stageableLines)
|
||||
assert.Equal(t, s.expectedHunkStarts, hunkStarts)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Vendored
-7
@@ -1,7 +0,0 @@
|
||||
diff --git a/blah b/blah
|
||||
new file mode 100644
|
||||
index 0000000..907b308
|
||||
--- /dev/null
|
||||
+++ b/blah
|
||||
@@ -0,0 +1 @@
|
||||
+blah
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
diff --git a/pkg/git/branch_list_builder.go b/pkg/git/branch_list_builder.go
|
||||
index 60ec4e0..db4485d 100644
|
||||
--- a/pkg/git/branch_list_builder.go
|
||||
+++ b/pkg/git/branch_list_builder.go
|
||||
@@ -14,8 +14,7 @@ import (
|
||||
|
||||
// context:
|
||||
// we want to only show 'safe' branches (ones that haven't e.g. been deleted)
|
||||
-// which `git branch -a` gives us, but we also want the recency data that
|
||||
// git reflog gives us.
|
||||
// So we get the HEAD, then append get the reflog branches that intersect with
|
||||
// our safe branches, then add the remaining safe branches, ensuring uniqueness
|
||||
// along the way
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
diff --git a/pkg/git/branch_list_builder.go b/pkg/git/branch_list_builder.go
|
||||
index 60ec4e0..db4485d 100644
|
||||
--- a/pkg/git/branch_list_builder.go
|
||||
+++ b/pkg/git/branch_list_builder.go
|
||||
@@ -14,8 +14,9 @@ import (
|
||||
|
||||
// context:
|
||||
// we want to only show 'safe' branches (ones that haven't e.g. been deleted)
|
||||
// which `git branch -a` gives us, but we also want the recency data that
|
||||
// git reflog gives us.
|
||||
+// test 2 - if I remove this, I decrement the end counter
|
||||
// So we get the HEAD, then append get the reflog branches that intersect with
|
||||
// our safe branches, then add the remaining safe branches, ensuring uniqueness
|
||||
// along the way
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
diff --git a/pkg/git/patch_modifier.go b/pkg/git/patch_modifier.go
|
||||
index a8fc600..6d8f7d7 100644
|
||||
--- a/pkg/git/patch_modifier.go
|
||||
+++ b/pkg/git/patch_modifier.go
|
||||
@@ -36,18 +36,19 @@ func (p *PatchModifier) ModifyPatchForHunk(patch string, hunkStarts []int, curre
|
||||
hunkEnd = hunkStarts[nextHunkStartIndex]
|
||||
}
|
||||
|
||||
headerLength := 4
|
||||
output := strings.Join(lines[0:headerLength], "\n") + "\n"
|
||||
output += strings.Join(lines[hunkStart:hunkEnd], "\n") + "\n"
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
+func getHeaderLength(patchLines []string) (int, error) {
|
||||
// ModifyPatchForLine takes the original patch, which may contain several hunks,
|
||||
// and the line number of the line we want to stage
|
||||
func (p *PatchModifier) ModifyPatchForLine(patch string, lineNumber int) (string, error) {
|
||||
lines := strings.Split(patch, "\n")
|
||||
headerLength := 4
|
||||
output := strings.Join(lines[0:headerLength], "\n") + "\n"
|
||||
|
||||
hunkStart, err := p.getHunkStart(lines, lineNumber)
|
||||
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
diff --git a/pkg/git/patch_modifier.go b/pkg/git/patch_modifier.go
|
||||
index a8fc600..6d8f7d7 100644
|
||||
--- a/pkg/git/patch_modifier.go
|
||||
+++ b/pkg/git/patch_modifier.go
|
||||
@@ -124,13 +140,14 @@ func (p *PatchModifier) getModifiedHunk(patchLines []string, hunkStart int, line
|
||||
// @@ -14,8 +14,9 @@ import (
|
||||
func (p *PatchModifier) updatedHeader(currentHeader string, lineChanges int) (string, error) {
|
||||
// current counter is the number after the second comma
|
||||
re := regexp.MustCompile(`^[^,]+,[^,]+,(\d+)`)
|
||||
matches := re.FindStringSubmatch(currentHeader)
|
||||
if len(matches) < 2 {
|
||||
re = regexp.MustCompile(`^[^,]+,[^+]+\+(\d+)`)
|
||||
matches = re.FindStringSubmatch(currentHeader)
|
||||
}
|
||||
prevLengthString := matches[1]
|
||||
+ prevLengthString := re.FindStringSubmatch(currentHeader)[1]
|
||||
|
||||
prevLength, err := strconv.Atoi(prevLengthString)
|
||||
if err != nil {
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
diff --git a/pkg/git/branch_list_builder.go b/pkg/git/branch_list_builder.go
|
||||
index 60ec4e0..db4485d 100644
|
||||
--- a/pkg/git/branch_list_builder.go
|
||||
+++ b/pkg/git/branch_list_builder.go
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
|
||||
// context:
|
||||
// we want to only show 'safe' branches (ones that haven't e.g. been deleted)
|
||||
-// which `git branch -a` gives us, but we also want the recency data that
|
||||
-// git reflog gives us.
|
||||
+// test 2 - if I remove this, I decrement the end counter
|
||||
+// test
|
||||
// So we get the HEAD, then append get the reflog branches that intersect with
|
||||
// our safe branches, then add the remaining safe branches, ensuring uniqueness
|
||||
// along the way
|
||||
-57
@@ -1,57 +0,0 @@
|
||||
diff --git a/pkg/git/patch_modifier.go b/pkg/git/patch_modifier.go
|
||||
index a8fc600..6d8f7d7 100644
|
||||
--- a/pkg/git/patch_modifier.go
|
||||
+++ b/pkg/git/patch_modifier.go
|
||||
@@ -36,18 +36,34 @@ func (p *PatchModifier) ModifyPatchForHunk(patch string, hunkStarts []int, curre
|
||||
hunkEnd = hunkStarts[nextHunkStartIndex]
|
||||
}
|
||||
|
||||
- headerLength := 4
|
||||
+ headerLength, err := getHeaderLength(lines)
|
||||
+ if err != nil {
|
||||
+ return "", err
|
||||
+ }
|
||||
+
|
||||
output := strings.Join(lines[0:headerLength], "\n") + "\n"
|
||||
output += strings.Join(lines[hunkStart:hunkEnd], "\n") + "\n"
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
+func getHeaderLength(patchLines []string) (int, error) {
|
||||
+ for index, line := range patchLines {
|
||||
+ if strings.HasPrefix(line, "@@") {
|
||||
+ return index, nil
|
||||
+ }
|
||||
+ }
|
||||
+ return 0, errors.New("Could not find any hunks in this patch")
|
||||
+}
|
||||
+
|
||||
// ModifyPatchForLine takes the original patch, which may contain several hunks,
|
||||
// and the line number of the line we want to stage
|
||||
func (p *PatchModifier) ModifyPatchForLine(patch string, lineNumber int) (string, error) {
|
||||
lines := strings.Split(patch, "\n")
|
||||
- headerLength := 4
|
||||
+ headerLength, err := getHeaderLength(lines)
|
||||
+ if err != nil {
|
||||
+ return "", err
|
||||
+ }
|
||||
output := strings.Join(lines[0:headerLength], "\n") + "\n"
|
||||
|
||||
hunkStart, err := p.getHunkStart(lines, lineNumber)
|
||||
@@ -124,13 +140,8 @@ func (p *PatchModifier) getModifiedHunk(patchLines []string, hunkStart int, line
|
||||
// @@ -14,8 +14,9 @@ import (
|
||||
func (p *PatchModifier) updatedHeader(currentHeader string, lineChanges int) (string, error) {
|
||||
// current counter is the number after the second comma
|
||||
- re := regexp.MustCompile(`^[^,]+,[^,]+,(\d+)`)
|
||||
- matches := re.FindStringSubmatch(currentHeader)
|
||||
- if len(matches) < 2 {
|
||||
- re = regexp.MustCompile(`^[^,]+,[^+]+\+(\d+)`)
|
||||
- matches = re.FindStringSubmatch(currentHeader)
|
||||
- }
|
||||
- prevLengthString := matches[1]
|
||||
+ re := regexp.MustCompile(`(\d+) @@`)
|
||||
+ prevLengthString := re.FindStringSubmatch(currentHeader)[1]
|
||||
|
||||
prevLength, err := strconv.Atoi(prevLengthString)
|
||||
if err != nil {
|
||||
+60
-23
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/git"
|
||||
)
|
||||
|
||||
// list panel functions
|
||||
@@ -20,21 +19,34 @@ func (gui *Gui) getSelectedBranch() *commands.Branch {
|
||||
return gui.State.Branches[selectedLine]
|
||||
}
|
||||
|
||||
func (gui *Gui) handleBranchesClick(g *gocui.Gui, v *gocui.View) error {
|
||||
itemCount := len(gui.State.Branches)
|
||||
handleSelect := gui.handleBranchSelect
|
||||
selectedLine := &gui.State.Panels.Branches.SelectedLine
|
||||
|
||||
return gui.handleClick(v, itemCount, selectedLine, handleSelect)
|
||||
}
|
||||
|
||||
// may want to standardise how these select methods work
|
||||
func (gui *Gui) handleBranchSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.State.SplitMainPanel = false
|
||||
|
||||
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.getMainView().Title = "Log"
|
||||
|
||||
// This really shouldn't happen: there should always be a master branch
|
||||
if len(gui.State.Branches) == 0 {
|
||||
return gui.renderString(g, "main", gui.Tr.SLocalize("NoBranchesThisRepo"))
|
||||
}
|
||||
branch := gui.getSelectedBranch()
|
||||
if err := gui.focusPoint(0, gui.State.Panels.Branches.SelectedLine, v); err != nil {
|
||||
if err := gui.focusPoint(0, gui.State.Panels.Branches.SelectedLine, len(gui.State.Branches), v); err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
@@ -67,7 +79,7 @@ func (gui *Gui) RenderSelectedBranchUpstreamDifferences() error {
|
||||
// be sure there is a state.Branches array to pick the current branch from
|
||||
func (gui *Gui) refreshBranches(g *gocui.Gui) error {
|
||||
g.Update(func(g *gocui.Gui) error {
|
||||
builder, err := git.NewBranchListBuilder(gui.Log, gui.GitCommand)
|
||||
builder, err := commands.NewBranchListBuilder(gui.Log, gui.GitCommand)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -121,15 +133,7 @@ func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("AlreadyCheckedOutBranch"))
|
||||
}
|
||||
branch := gui.getSelectedBranch()
|
||||
if err := gui.GitCommand.Checkout(branch.Name, false); err != nil {
|
||||
if err := gui.createErrorPanel(g, err.Error()); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
gui.State.Panels.Branches.SelectedLine = 0
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(g)
|
||||
return gui.handleCheckoutBranch(branch.Name)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreatePullRequestPress(g *gocui.Gui, v *gocui.View) error {
|
||||
@@ -158,7 +162,7 @@ func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
|
||||
branch := gui.getSelectedBranch()
|
||||
message := gui.Tr.SLocalize("SureForceCheckout")
|
||||
title := gui.Tr.SLocalize("ForceCheckoutBranch")
|
||||
return gui.createConfirmationPanel(g, v, title, message, func(g *gocui.Gui, v *gocui.View) error {
|
||||
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())
|
||||
}
|
||||
@@ -166,12 +170,45 @@ func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
|
||||
}, nil)
|
||||
}
|
||||
|
||||
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 {
|
||||
if err := gui.GitCommand.Checkout(gui.trimmedContent(v), false); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
func (gui *Gui) handleCheckoutBranch(branchName string) error {
|
||||
if err := gui.GitCommand.Checkout(branchName, false); err != nil {
|
||||
// note, this will only work for english-language git commands. If we force git to use english, and the error isn't this one, then the user will receive an english command they may not understand. I'm not sure what the best solution to this is. Running the command once in english and a second time in the native language is one option
|
||||
|
||||
if strings.Contains(err.Error(), "Please commit your changes or stash them before you switch branch") {
|
||||
// offer to autostash changes
|
||||
return gui.createConfirmationPanel(gui.g, gui.getBranchesView(), true, gui.Tr.SLocalize("AutoStashTitle"), gui.Tr.SLocalize("AutoStashPrompt"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.StashSave(gui.Tr.SLocalize("StashPrefix") + branchName); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
if err := gui.GitCommand.Checkout(branchName, 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)
|
||||
}
|
||||
return gui.refreshSidePanels(g)
|
||||
|
||||
if err := gui.createErrorPanel(gui.g, err.Error()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
gui.State.Panels.Branches.SelectedLine = 0
|
||||
return gui.refreshSidePanels(gui.g)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCheckoutByName(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.createPromptPanel(g, v, gui.Tr.SLocalize("BranchName")+":", "", func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.handleCheckoutBranch(gui.trimmedContent(v))
|
||||
})
|
||||
return nil
|
||||
}
|
||||
@@ -184,7 +221,7 @@ func (gui *Gui) handleNewBranch(g *gocui.Gui, v *gocui.View) error {
|
||||
"branchName": branch.Name,
|
||||
},
|
||||
)
|
||||
gui.createPromptPanel(g, v, message, func(g *gocui.Gui, v *gocui.View) error {
|
||||
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())
|
||||
}
|
||||
@@ -228,7 +265,7 @@ func (gui *Gui) deleteNamedBranch(g *gocui.Gui, v *gocui.View, selectedBranch *c
|
||||
"selectedBranchName": selectedBranch.Name,
|
||||
},
|
||||
)
|
||||
return gui.createConfirmationPanel(g, v, title, message, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.createConfirmationPanel(g, v, true, title, message, func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.DeleteBranch(selectedBranch.Name, force); err != nil {
|
||||
errMessage := err.Error()
|
||||
if !force && strings.Contains(errMessage, "is not fully merged") {
|
||||
@@ -253,7 +290,7 @@ func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error {
|
||||
"selectedBranch": selectedBranch,
|
||||
},
|
||||
)
|
||||
return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("MergingTitle"), prompt,
|
||||
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("MergingTitle"), prompt,
|
||||
func(g *gocui.Gui, v *gocui.View) error {
|
||||
|
||||
err := gui.GitCommand.Merge(selectedBranch)
|
||||
@@ -274,7 +311,7 @@ func (gui *Gui) handleRebase(g *gocui.Gui, v *gocui.View) error {
|
||||
"selectedBranch": selectedBranch,
|
||||
},
|
||||
)
|
||||
return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("RebasingTitle"), prompt,
|
||||
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("RebasingTitle"), prompt,
|
||||
func(g *gocui.Gui, v *gocui.View) error {
|
||||
|
||||
err := gui.GitCommand.RebaseBranch(selectedBranch)
|
||||
@@ -309,7 +346,7 @@ func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.FastForward(branch.Name); err != nil {
|
||||
_ = gui.createErrorPanel(gui.g, err.Error())
|
||||
} else {
|
||||
_ = gui.closeConfirmationPrompt(gui.g)
|
||||
_ = gui.closeConfirmationPrompt(gui.g, true)
|
||||
_ = gui.RenderSelectedBranchUpstreamDifferences()
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
)
|
||||
|
||||
func (gui *Gui) getSelectedCommitFile(g *gocui.Gui) *commands.CommitFile {
|
||||
selectedLine := gui.State.Panels.CommitFiles.SelectedLine
|
||||
if selectedLine == -1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.State.CommitFiles[selectedLine]
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitFilesClick(g *gocui.Gui, v *gocui.View) error {
|
||||
itemCount := len(gui.State.CommitFiles)
|
||||
handleSelect := gui.handleCommitFileSelect
|
||||
selectedLine := &gui.State.Panels.CommitFiles.SelectedLine
|
||||
|
||||
return gui.handleClick(v, itemCount, selectedLine, handleSelect)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitFileSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.getMainView().Title = "Patch"
|
||||
gui.State.Panels.LineByLine = nil
|
||||
|
||||
commitFile := gui.getSelectedCommitFile(g)
|
||||
if commitFile == nil {
|
||||
return gui.renderString(g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
|
||||
}
|
||||
|
||||
if err := gui.refreshSecondaryPatchPanel(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.focusPoint(0, gui.State.Panels.CommitFiles.SelectedLine, len(gui.State.CommitFiles), v); err != nil {
|
||||
return err
|
||||
}
|
||||
commitText, err := gui.GitCommand.ShowCommitFile(commitFile.Sha, commitFile.Name, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.renderString(g, "main", commitText)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitFilesNextLine(g *gocui.Gui, v *gocui.View) error {
|
||||
panelState := gui.State.Panels.CommitFiles
|
||||
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.CommitFiles), false)
|
||||
|
||||
return gui.handleCommitFileSelect(gui.g, v)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitFilesPrevLine(g *gocui.Gui, v *gocui.View) error {
|
||||
panelState := gui.State.Panels.CommitFiles
|
||||
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.CommitFiles), true)
|
||||
|
||||
return gui.handleCommitFileSelect(gui.g, v)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSwitchToCommitsPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
commitsView, err := g.View("commits")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.switchFocus(g, v, commitsView)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCheckoutCommitFile(g *gocui.Gui, v *gocui.View) error {
|
||||
file := gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLine]
|
||||
|
||||
if err := gui.GitCommand.CheckoutFile(file.Sha, file.Name); err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
}
|
||||
|
||||
return gui.refreshFiles()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleDiscardOldFileChange(g *gocui.Gui, v *gocui.View) error {
|
||||
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
fileName := gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLine].Name
|
||||
|
||||
return gui.createConfirmationPanel(gui.g, v, true, gui.Tr.SLocalize("DiscardFileChangesTitle"), gui.Tr.SLocalize("DiscardFileChangesPrompt"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
|
||||
if err := gui.GitCommand.DiscardOldFileChanges(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, fileName); err != nil {
|
||||
if err := gui.handleGenericMergeCommandResult(err); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(gui.g)
|
||||
})
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshCommitFilesView() error {
|
||||
if err := gui.refreshSecondaryPatchPanel(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.refreshPatchBuildingPanel(-1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
commit := gui.getSelectedCommit(gui.g)
|
||||
if commit == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
files, err := gui.GitCommand.GetCommitFiles(commit.Sha, gui.GitCommand.PatchManager)
|
||||
if err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
}
|
||||
gui.State.CommitFiles = files
|
||||
|
||||
gui.refreshSelectedLine(&gui.State.Panels.CommitFiles.SelectedLine, len(gui.State.CommitFiles))
|
||||
|
||||
if err := gui.renderListPanel(gui.getCommitFilesView(), gui.State.CommitFiles); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.handleCommitFileSelect(gui.g, gui.getCommitFilesView())
|
||||
}
|
||||
|
||||
func (gui *Gui) handleOpenOldCommitFile(g *gocui.Gui, v *gocui.View) error {
|
||||
file := gui.getSelectedCommitFile(g)
|
||||
return gui.openFile(file.Name)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleToggleFileForPatch(g *gocui.Gui, v *gocui.View) error {
|
||||
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
commitFile := gui.getSelectedCommitFile(g)
|
||||
if commitFile == nil {
|
||||
return gui.renderString(g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
|
||||
}
|
||||
|
||||
toggleTheFile := func() error {
|
||||
if !gui.GitCommand.PatchManager.CommitSelected() {
|
||||
if err := gui.startPatchManager(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
gui.GitCommand.PatchManager.ToggleFileWhole(commitFile.Name)
|
||||
|
||||
return gui.refreshCommitFilesView()
|
||||
}
|
||||
|
||||
if gui.GitCommand.PatchManager.CommitSelected() && gui.GitCommand.PatchManager.CommitSha != commitFile.Sha {
|
||||
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("DiscardPatch"), gui.Tr.SLocalize("DiscardPatchConfirm"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.GitCommand.PatchManager.Reset()
|
||||
return toggleTheFile()
|
||||
}, nil)
|
||||
}
|
||||
|
||||
return toggleTheFile()
|
||||
}
|
||||
|
||||
func (gui *Gui) startPatchManager() error {
|
||||
diffMap := map[string]string{}
|
||||
for _, commitFile := range gui.State.CommitFiles {
|
||||
commitText, err := gui.GitCommand.ShowCommitFile(commitFile.Sha, commitFile.Name, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
diffMap[commitFile.Name] = commitText
|
||||
}
|
||||
|
||||
commit := gui.getSelectedCommit(gui.g)
|
||||
if commit == nil {
|
||||
return errors.New("No commit selected")
|
||||
}
|
||||
|
||||
gui.GitCommand.PatchManager.Start(commit.Sha, diffMap)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleEnterCommitFile(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.enterCommitFile(-1)
|
||||
}
|
||||
|
||||
func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
|
||||
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
commitFile := gui.getSelectedCommitFile(gui.g)
|
||||
if commitFile == nil {
|
||||
return gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
|
||||
}
|
||||
|
||||
enterTheFile := func(selectedLineIdx int) error {
|
||||
if !gui.GitCommand.PatchManager.CommitSelected() {
|
||||
if err := gui.startPatchManager(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := gui.changeContext("patch-building"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.switchFocus(gui.g, gui.getCommitFilesView(), gui.getMainView()); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.refreshPatchBuildingPanel(selectedLineIdx)
|
||||
}
|
||||
|
||||
if gui.GitCommand.PatchManager.CommitSelected() && gui.GitCommand.PatchManager.CommitSha != commitFile.Sha {
|
||||
return gui.createConfirmationPanel(gui.g, gui.getCommitFilesView(), false, gui.Tr.SLocalize("DiscardPatch"), gui.Tr.SLocalize("DiscardPatchConfirm"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.GitCommand.PatchManager.Reset()
|
||||
return enterTheFile(selectedLineIdx)
|
||||
}, nil)
|
||||
}
|
||||
|
||||
return enterTheFile(selectedLineIdx)
|
||||
}
|
||||
@@ -11,17 +11,18 @@ import (
|
||||
// runSyncOrAsyncCommand takes the output of a command that may have returned
|
||||
// either no error, an error, or a subprocess to execute, and if a subprocess
|
||||
// needs to be set on the gui object, it does so, and then returns the error
|
||||
func (gui *Gui) runSyncOrAsyncCommand(sub *exec.Cmd, err error) error {
|
||||
// the bool returned tells us whether the calling code should continue
|
||||
func (gui *Gui) runSyncOrAsyncCommand(sub *exec.Cmd, err error) (bool, error) {
|
||||
if err != nil {
|
||||
if err != gui.Errors.ErrSubProcess {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
return false, gui.createErrorPanel(gui.g, err.Error())
|
||||
}
|
||||
}
|
||||
if sub != nil {
|
||||
gui.SubProcess = sub
|
||||
return gui.Errors.ErrSubProcess
|
||||
return false, gui.Errors.ErrSubProcess
|
||||
}
|
||||
return nil
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
|
||||
@@ -29,9 +30,18 @@ func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
|
||||
if message == "" {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("CommitWithoutMessageErr"))
|
||||
}
|
||||
if err := gui.runSyncOrAsyncCommand(gui.GitCommand.Commit(message)); err != nil {
|
||||
flags := ""
|
||||
skipHookPrefix := gui.Config.GetUserConfig().GetString("git.skipHookPrefix")
|
||||
if skipHookPrefix != "" && strings.HasPrefix(message, skipHookPrefix) {
|
||||
flags = "--no-verify"
|
||||
}
|
||||
ok, err := gui.runSyncOrAsyncCommand(gui.GitCommand.Commit(message, flags))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
v.Clear()
|
||||
_ = v.SetCursor(0, 0)
|
||||
@@ -61,33 +71,6 @@ func (gui *Gui) handleCommitFocused(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.renderString(g, "options", message)
|
||||
}
|
||||
|
||||
func (gui *Gui) simpleEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
|
||||
switch {
|
||||
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
|
||||
v.EditDelete(true)
|
||||
case key == gocui.KeyDelete:
|
||||
v.EditDelete(false)
|
||||
case key == gocui.KeyArrowDown:
|
||||
v.MoveCursor(0, 1, false)
|
||||
case key == gocui.KeyArrowUp:
|
||||
v.MoveCursor(0, -1, false)
|
||||
case key == gocui.KeyArrowLeft:
|
||||
v.MoveCursor(-1, 0, false)
|
||||
case key == gocui.KeyArrowRight:
|
||||
v.MoveCursor(1, 0, false)
|
||||
case key == gocui.KeyTab:
|
||||
v.EditNewLine()
|
||||
case key == gocui.KeySpace:
|
||||
v.EditWrite(' ')
|
||||
case key == gocui.KeyInsert:
|
||||
v.Overwrite = !v.Overwrite
|
||||
default:
|
||||
v.EditWrite(ch)
|
||||
}
|
||||
|
||||
gui.RenderCommitLength()
|
||||
}
|
||||
|
||||
func (gui *Gui) getBufferLength(view *gocui.View) string {
|
||||
return " " + strconv.Itoa(strings.Count(view.Buffer(), "")-1) + " "
|
||||
}
|
||||
|
||||
+208
-12
@@ -4,11 +4,11 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/git"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -23,22 +23,59 @@ func (gui *Gui) getSelectedCommit(g *gocui.Gui) *commands.Commit {
|
||||
return gui.State.Commits[selectedLine]
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitsClick(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
prevSelectedLineIdx := gui.State.Panels.Commits.SelectedLine
|
||||
newSelectedLineIdx := v.SelectedLineIdx()
|
||||
|
||||
if newSelectedLineIdx > len(gui.State.Commits)-1 {
|
||||
return gui.handleCommitSelect(gui.g, v)
|
||||
}
|
||||
|
||||
gui.State.Panels.Commits.SelectedLine = newSelectedLineIdx
|
||||
|
||||
if prevSelectedLineIdx == newSelectedLineIdx && gui.currentViewName() == v.Name() {
|
||||
return gui.handleSwitchToCommitFilesPanel(gui.g, v)
|
||||
} else {
|
||||
return gui.handleCommitSelect(gui.g, v)
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// this probably belongs in an 'onFocus' function than a 'commit selected' function
|
||||
if err := gui.refreshSecondaryPatchPanel(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.getMainView().Title = "Patch"
|
||||
gui.getSecondaryView().Title = "Custom Patch"
|
||||
gui.State.Panels.LineByLine = nil
|
||||
|
||||
commit := gui.getSelectedCommit(g)
|
||||
if commit == nil {
|
||||
return gui.renderString(g, "main", gui.Tr.SLocalize("NoCommitsThisBranch"))
|
||||
}
|
||||
|
||||
if err := gui.focusPoint(0, gui.State.Panels.Commits.SelectedLine, v); err != nil {
|
||||
if err := gui.focusPoint(0, gui.State.Panels.Commits.SelectedLine, len(gui.State.Commits), v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if specific diff mode is on, don't show diff
|
||||
if gui.State.Panels.Commits.SpecificDiffMode {
|
||||
return nil
|
||||
}
|
||||
|
||||
commitText, err := gui.GitCommand.Show(commit.Sha)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -48,7 +85,7 @@ func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
|
||||
func (gui *Gui) refreshCommits(g *gocui.Gui) error {
|
||||
g.Update(func(*gocui.Gui) error {
|
||||
builder, err := git.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr, gui.State.CherryPickedCommits)
|
||||
builder, err := commands.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr, gui.State.CherryPickedCommits, gui.State.DiffEntries)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -71,9 +108,12 @@ func (gui *Gui) refreshCommits(g *gocui.Gui) error {
|
||||
fmt.Fprint(v, list)
|
||||
|
||||
gui.refreshStatus(g)
|
||||
if v == g.CurrentView() {
|
||||
if g.CurrentView() == v {
|
||||
gui.handleCommitSelect(g, v)
|
||||
}
|
||||
if g.CurrentView() == gui.getCommitFilesView() || (g.CurrentView() == gui.getMainView() || gui.State.Context == "patch-building") {
|
||||
return gui.refreshCommitFilesView()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
@@ -110,12 +150,13 @@ func (gui *Gui) handleCommitsPrevLine(g *gocui.Gui, v *gocui.View) error {
|
||||
// specific functions
|
||||
|
||||
func (gui *Gui) handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error {
|
||||
return gui.createConfirmationPanel(g, commitView, gui.Tr.SLocalize("ResetToCommit"), gui.Tr.SLocalize("SureResetThisCommit"), func(g *gocui.Gui, v *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); err != nil {
|
||||
|
||||
if err := gui.GitCommand.ResetToCommit(commit.Sha, "mixed"); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
if err := gui.refreshCommits(g); err != nil {
|
||||
@@ -143,7 +184,7 @@ func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("Squash"), gui.Tr.SLocalize("SureSquashThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
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)
|
||||
@@ -175,7 +216,7 @@ func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("Fixup"), gui.Tr.SLocalize("SureFixupThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
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)
|
||||
@@ -196,7 +237,7 @@ 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.createPromptPanel(g, v, gui.Tr.SLocalize("renameCommit"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
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())
|
||||
}
|
||||
@@ -275,7 +316,7 @@ func (gui *Gui) handleCommitDelete(g *gocui.Gui, v *gocui.View) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.createConfirmationPanel(gui.g, v, gui.Tr.SLocalize("DeleteCommitTitle"), gui.Tr.SLocalize("DeleteCommitPrompt"), func(*gocui.Gui, *gocui.View) error {
|
||||
return gui.createConfirmationPanel(gui.g, v, true, gui.Tr.SLocalize("DeleteCommitTitle"), gui.Tr.SLocalize("DeleteCommitPrompt"), func(*gocui.Gui, *gocui.View) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.SLocalize("DeletingStatus"), func() error {
|
||||
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "drop")
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
@@ -345,7 +386,7 @@ func (gui *Gui) handleCommitEdit(g *gocui.Gui, v *gocui.View) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitAmendTo(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.createConfirmationPanel(gui.g, v, gui.Tr.SLocalize("AmendCommitTitle"), gui.Tr.SLocalize("AmendCommitPrompt"), func(*gocui.Gui, *gocui.View) error {
|
||||
return gui.createConfirmationPanel(gui.g, v, true, gui.Tr.SLocalize("AmendCommitTitle"), gui.Tr.SLocalize("AmendCommitPrompt"), func(*gocui.Gui, *gocui.View) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.SLocalize("AmendingStatus"), func() error {
|
||||
err := gui.GitCommand.AmendTo(gui.State.Commits[gui.State.Panels.Commits.SelectedLine].Sha)
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
@@ -430,10 +471,165 @@ func (gui *Gui) handleCopyCommitRange(g *gocui.Gui, v *gocui.View) error {
|
||||
|
||||
// HandlePasteCommits begins a cherry-pick rebase with the commits the user has copied
|
||||
func (gui *Gui) HandlePasteCommits(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("CherryPick"), gui.Tr.SLocalize("SureCherryPick"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("CherryPick"), gui.Tr.SLocalize("SureCherryPick"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.SLocalize("CherryPickingStatus"), func() error {
|
||||
err := gui.GitCommand.CherryPickCommits(gui.State.CherryPickedCommits)
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSwitchToCommitFilesPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.refreshCommitFilesView(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.switchFocus(g, gui.getCommitsView(), gui.getCommitFilesView())
|
||||
}
|
||||
|
||||
func (gui *Gui) handleToggleDiffCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
selectLimit := 2
|
||||
|
||||
// get selected commit
|
||||
commit := gui.getSelectedCommit(g)
|
||||
if commit == nil {
|
||||
return gui.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 {
|
||||
return idx, true
|
||||
}
|
||||
}
|
||||
return -1, false
|
||||
}
|
||||
|
||||
func (gui *Gui) unchooseCommit(commits []*commands.Commit, i int) []*commands.Commit {
|
||||
return append(commits[:i], commits[i+1:]...)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateFixupCommit(g *gocui.Gui, v *gocui.View) error {
|
||||
commit := gui.getSelectedCommit(g)
|
||||
if commit == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("CreateFixupCommit"), gui.Tr.TemplateLocalize(
|
||||
"SureCreateFixupCommit",
|
||||
Teml{
|
||||
"commit": commit.Sha,
|
||||
},
|
||||
), func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.CreateFixupCommit(commit.Sha); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(gui.g)
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSquashAllAboveFixupCommits(g *gocui.Gui, v *gocui.View) error {
|
||||
commit := gui.getSelectedCommit(g)
|
||||
if commit == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("SquashAboveCommits"), gui.Tr.TemplateLocalize(
|
||||
"SureSquashAboveCommits",
|
||||
Teml{
|
||||
"commit": commit.Sha,
|
||||
},
|
||||
), func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.SLocalize("SquashingStatus"), func() error {
|
||||
err := gui.GitCommand.SquashAllAboveFixupCommits(commit.Sha)
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
}, nil)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -12,26 +12,29 @@ import (
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
)
|
||||
|
||||
func (gui *Gui) wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.View) error) func(*gocui.Gui, *gocui.View) error {
|
||||
func (gui *Gui) wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.View) error, returnFocusOnClose bool) func(*gocui.Gui, *gocui.View) error {
|
||||
return func(g *gocui.Gui, v *gocui.View) error {
|
||||
if function != nil {
|
||||
if err := function(g, v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return gui.closeConfirmationPrompt(g)
|
||||
return gui.closeConfirmationPrompt(g, returnFocusOnClose)
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) closeConfirmationPrompt(g *gocui.Gui) error {
|
||||
func (gui *Gui) closeConfirmationPrompt(g *gocui.Gui, returnFocusOnClose bool) error {
|
||||
view, err := g.View("confirmation")
|
||||
if err != nil {
|
||||
return nil // if it's already been closed we can just return
|
||||
}
|
||||
if err := gui.returnFocus(g, view); err != nil {
|
||||
panic(err)
|
||||
if returnFocusOnClose {
|
||||
if err := gui.returnFocus(g, view); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
g.DeleteKeybindings("confirmation")
|
||||
return g.DeleteView("confirmation")
|
||||
@@ -53,7 +56,7 @@ func (gui *Gui) getMessageHeight(wrap bool, message string, width int) int {
|
||||
|
||||
func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, wrap bool, prompt string) (int, int, int, int) {
|
||||
width, height := g.Size()
|
||||
panelWidth := width / 2
|
||||
panelWidth := 4 * width / 7
|
||||
panelHeight := gui.getMessageHeight(wrap, prompt, panelWidth)
|
||||
return width/2 - panelWidth/2,
|
||||
height/2 - panelHeight/2 - panelHeight%2 - 1,
|
||||
@@ -61,14 +64,18 @@ func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, wrap bool, prompt s
|
||||
height/2 + panelHeight/2
|
||||
}
|
||||
|
||||
func (gui *Gui) createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, handleConfirm func(*gocui.Gui, *gocui.View) error) error {
|
||||
func (gui *Gui) createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, initialContent string, handleConfirm func(*gocui.Gui, *gocui.View) error) error {
|
||||
gui.onNewPopupPanel()
|
||||
confirmationView, err := gui.prepareConfirmationPanel(currentView, title, "", false)
|
||||
confirmationView, err := gui.prepareConfirmationPanel(currentView, title, initialContent, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
confirmationView.Editable = true
|
||||
return gui.setKeyBindings(g, handleConfirm, nil)
|
||||
if err := gui.renderString(g, "confirmation", initialContent); err != nil {
|
||||
return err
|
||||
}
|
||||
// in the future we might want to give createPromptPanel the returnFocusOnClose arg too, but for now we're always setting it to true
|
||||
return gui.setKeyBindings(g, handleConfirm, nil, true)
|
||||
}
|
||||
|
||||
func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt string, hasLoader bool) (*gocui.View, error) {
|
||||
@@ -81,7 +88,7 @@ func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt
|
||||
confirmationView.HasLoader = hasLoader
|
||||
confirmationView.Title = title
|
||||
confirmationView.Wrap = true
|
||||
confirmationView.FgColor = gocui.ColorWhite
|
||||
confirmationView.FgColor = theme.GocuiDefaultTextColor
|
||||
}
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
return gui.switchFocus(gui.g, currentView, confirmationView)
|
||||
@@ -99,20 +106,20 @@ func (gui *Gui) onNewPopupPanel() {
|
||||
}
|
||||
|
||||
func (gui *Gui) createLoaderPanel(g *gocui.Gui, currentView *gocui.View, prompt string) error {
|
||||
return gui.createPopupPanel(g, currentView, "", prompt, true, nil, nil)
|
||||
return gui.createPopupPanel(g, currentView, "", prompt, true, true, nil, nil)
|
||||
}
|
||||
|
||||
// it is very important that within this function we never include the original prompt in any error messages, because it may contain e.g. a user password
|
||||
func (gui *Gui) createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
|
||||
return gui.createPopupPanel(g, currentView, title, prompt, false, handleConfirm, handleClose)
|
||||
func (gui *Gui) createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, returnFocusOnClose bool, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
|
||||
return gui.createPopupPanel(g, currentView, title, prompt, false, returnFocusOnClose, handleConfirm, handleClose)
|
||||
}
|
||||
|
||||
func (gui *Gui) createPopupPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, hasLoader bool, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
|
||||
func (gui *Gui) createPopupPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, hasLoader bool, returnFocusOnClose bool, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
|
||||
gui.onNewPopupPanel()
|
||||
g.Update(func(g *gocui.Gui) error {
|
||||
// delete the existing confirmation panel if it exists
|
||||
if view, _ := g.View("confirmation"); view != nil {
|
||||
if err := gui.closeConfirmationPrompt(g); err != nil {
|
||||
if err := gui.closeConfirmationPrompt(g, true); err != nil {
|
||||
errMessage := gui.Tr.TemplateLocalize(
|
||||
"CantCloseConfirmationPrompt",
|
||||
Teml{
|
||||
@@ -130,12 +137,12 @@ func (gui *Gui) createPopupPanel(g *gocui.Gui, currentView *gocui.View, title, p
|
||||
if err := gui.renderString(g, "confirmation", prompt); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.setKeyBindings(g, handleConfirm, handleClose)
|
||||
return gui.setKeyBindings(g, handleConfirm, handleClose, returnFocusOnClose)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
|
||||
func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error, returnFocusOnClose bool) error {
|
||||
actions := gui.Tr.TemplateLocalize(
|
||||
"CloseConfirm",
|
||||
Teml{
|
||||
@@ -146,14 +153,14 @@ func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*go
|
||||
if err := gui.renderString(g, "options", actions); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := g.SetKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone, gui.wrappedConfirmationFunction(handleConfirm)); err != nil {
|
||||
if err := g.SetKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone, gui.wrappedConfirmationFunction(handleConfirm, returnFocusOnClose)); err != nil {
|
||||
return err
|
||||
}
|
||||
return g.SetKeybinding("confirmation", gocui.KeyEsc, gocui.ModNone, gui.wrappedConfirmationFunction(handleClose))
|
||||
return g.SetKeybinding("confirmation", 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, nil, nil)
|
||||
return gui.createPopupPanel(g, currentView, title, prompt, false, true, nil, nil)
|
||||
}
|
||||
|
||||
// createSpecificErrorPanel allows you to create an error popup, specifying the
|
||||
@@ -174,7 +181,7 @@ func (gui *Gui) createSpecificErrorPanel(message string, nextView *gocui.View, w
|
||||
|
||||
colorFunction := color.New(color.FgRed).SprintFunc()
|
||||
coloredMessage := colorFunction(strings.TrimSpace(message))
|
||||
return gui.createConfirmationPanel(gui.g, nextView, gui.Tr.SLocalize("Error"), coloredMessage, nil, nil)
|
||||
return gui.createConfirmationPanel(gui.g, nextView, true, gui.Tr.SLocalize("Error"), coloredMessage, nil, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) createErrorPanel(g *gocui.Gui, message string) error {
|
||||
|
||||
+27
-57
@@ -1,75 +1,45 @@
|
||||
package gui
|
||||
|
||||
func (gui *Gui) titleMap() map[string]string {
|
||||
return map[string]string{
|
||||
"commits": gui.Tr.SLocalize("DiffTitle"),
|
||||
"branches": gui.Tr.SLocalize("LogTitle"),
|
||||
"files": gui.Tr.SLocalize("DiffTitle"),
|
||||
"status": "",
|
||||
"stash": gui.Tr.SLocalize("DiffTitle"),
|
||||
}
|
||||
}
|
||||
func (gui *Gui) changeContext(context string) error {
|
||||
oldContext := gui.State.Context
|
||||
|
||||
func (gui *Gui) contextTitleMap() map[string]map[string]string {
|
||||
return map[string]map[string]string{
|
||||
"main": {
|
||||
"staging": gui.Tr.SLocalize("StagingMainTitle"),
|
||||
"merging": gui.Tr.SLocalize("MergingMainTitle"),
|
||||
"normal": "",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) setMainTitle() error {
|
||||
currentViewName := gui.g.CurrentView().Name()
|
||||
var newTitle string
|
||||
if context, ok := gui.State.Contexts[currentViewName]; ok {
|
||||
newTitle = gui.contextTitleMap()[currentViewName][context]
|
||||
} else if title, ok := gui.titleMap()[currentViewName]; ok {
|
||||
newTitle = title
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
gui.getMainView().Title = newTitle
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) changeContext(viewName, context string) error {
|
||||
if gui.State.Contexts[viewName] == context {
|
||||
if gui.State.Context == context {
|
||||
return nil
|
||||
}
|
||||
|
||||
contextMap := gui.GetContextMap()
|
||||
|
||||
gui.g.DeleteKeybindings(viewName)
|
||||
|
||||
bindings := contextMap[viewName][context]
|
||||
for _, binding := range bindings {
|
||||
if err := gui.g.SetKeybinding(viewName, binding.Key, binding.Modifier, binding.Handler); err != nil {
|
||||
oldBindings := contextMap[oldContext]
|
||||
for _, binding := range oldBindings {
|
||||
if err := gui.g.DeleteKeybinding(binding.ViewName, binding.Key, binding.Modifier); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
gui.State.Contexts[viewName] = context
|
||||
return gui.setMainTitle()
|
||||
}
|
||||
|
||||
func (gui *Gui) setInitialContexts() error {
|
||||
contextMap := gui.GetContextMap()
|
||||
|
||||
initialContexts := map[string]string{
|
||||
"main": "normal",
|
||||
}
|
||||
|
||||
for viewName, context := range initialContexts {
|
||||
bindings := contextMap[viewName][context]
|
||||
for _, binding := range bindings {
|
||||
if err := gui.g.SetKeybinding(binding.ViewName, binding.Key, binding.Modifier, binding.Handler); err != nil {
|
||||
return err
|
||||
}
|
||||
bindings := contextMap[context]
|
||||
for _, binding := range bindings {
|
||||
if err := gui.g.SetKeybinding(binding.ViewName, binding.Key, binding.Modifier, binding.Handler); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
gui.State.Contexts = initialContexts
|
||||
gui.State.Context = context
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) setInitialContext() error {
|
||||
contextMap := gui.GetContextMap()
|
||||
|
||||
initialContext := "normal"
|
||||
|
||||
bindings := contextMap[initialContext]
|
||||
for _, binding := range bindings {
|
||||
if err := gui.g.SetKeybinding(binding.ViewName, binding.Key, binding.Modifier, binding.Handler); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
gui.State.Context = initialContext
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ 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)
|
||||
_ = gui.closeConfirmationPrompt(g, true)
|
||||
_ = gui.refreshSidePanels(g)
|
||||
}
|
||||
}
|
||||
|
||||
+265
-98
@@ -10,6 +10,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
@@ -26,24 +27,21 @@ func (gui *Gui) getSelectedFile(g *gocui.Gui) (*commands.File, error) {
|
||||
return gui.State.Files[selectedLine], nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleFilesFocus(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleFilesClick(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
cx, cy := v.Cursor()
|
||||
_, oy := v.Origin()
|
||||
prevSelectedLineIdx := gui.State.Panels.Files.SelectedLine
|
||||
newSelectedLineIdx := v.SelectedLineIdx()
|
||||
|
||||
prevSelectedLine := gui.State.Panels.Files.SelectedLine
|
||||
newSelectedLine := cy - oy
|
||||
|
||||
if newSelectedLine > len(gui.State.Files)-1 || len(utils.Decolorise(gui.State.Files[newSelectedLine].DisplayString)) < cx {
|
||||
if newSelectedLineIdx > len(gui.State.Files)-1 {
|
||||
return gui.handleFileSelect(gui.g, v, false)
|
||||
}
|
||||
|
||||
gui.State.Panels.Files.SelectedLine = newSelectedLine
|
||||
gui.State.Panels.Files.SelectedLine = newSelectedLineIdx
|
||||
|
||||
if prevSelectedLine == newSelectedLine && gui.currentViewName() == v.Name() {
|
||||
if prevSelectedLineIdx == newSelectedLineIdx && gui.currentViewName() == v.Name() {
|
||||
return gui.handleFilePress(gui.g, v)
|
||||
} else {
|
||||
return gui.handleFileSelect(gui.g, v, true)
|
||||
@@ -63,7 +61,7 @@ func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View, alreadySelected bo
|
||||
return gui.renderString(g, "main", gui.Tr.SLocalize("NoChangedFiles"))
|
||||
}
|
||||
|
||||
if err := gui.focusPoint(0, gui.State.Panels.Files.SelectedLine, v); err != nil {
|
||||
if err := gui.focusPoint(0, gui.State.Panels.Files.SelectedLine, len(gui.State.Files), v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -71,20 +69,47 @@ func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View, alreadySelected bo
|
||||
return gui.refreshMergePanel()
|
||||
}
|
||||
|
||||
content := gui.GitCommand.Diff(file, false)
|
||||
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")
|
||||
} 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 {
|
||||
g.Update(func(*gocui.Gui) error {
|
||||
return gui.setViewContent(gui.g, gui.getMainView(), content)
|
||||
if err := gui.setViewContent(gui.g, gui.getSecondaryView(), contentCached); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.setViewContent(gui.g, gui.getMainView(), leftContent)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
return gui.renderString(g, "main", content)
|
||||
if err := gui.renderString(g, "secondary", contentCached); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.renderString(g, "main", leftContent)
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshFiles() error {
|
||||
selectedFile, _ := gui.getSelectedFile(gui.g)
|
||||
|
||||
filesView := gui.getFilesView()
|
||||
if filesView == nil {
|
||||
// if the filesView hasn't been instantiated yet we just return
|
||||
return nil
|
||||
}
|
||||
if err := gui.refreshStateFiles(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -165,7 +190,11 @@ func (gui *Gui) stageSelectedFile(g *gocui.Gui) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleEnterFile(g *gocui.Gui, v *gocui.View) error {
|
||||
file, err := gui.getSelectedFile(g)
|
||||
return gui.enterFile(false, -1)
|
||||
}
|
||||
|
||||
func (gui *Gui) enterFile(forceSecondaryFocused bool, selectedLineIdx int) error {
|
||||
file, err := gui.getSelectedFile(gui.g)
|
||||
if err != nil {
|
||||
if err != gui.Errors.ErrNoFiles {
|
||||
return err
|
||||
@@ -173,18 +202,18 @@ func (gui *Gui) handleEnterFile(g *gocui.Gui, v *gocui.View) error {
|
||||
return nil
|
||||
}
|
||||
if file.HasInlineMergeConflicts {
|
||||
return gui.handleSwitchToMerge(g, v)
|
||||
return gui.handleSwitchToMerge(gui.g, gui.getFilesView())
|
||||
}
|
||||
if !file.HasUnstagedChanges || file.HasMergeConflicts {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("FileStagingRequirements"))
|
||||
if file.HasMergeConflicts {
|
||||
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("FileStagingRequirements"))
|
||||
}
|
||||
if err := gui.changeContext("main", "staging"); err != nil {
|
||||
if err := gui.changeContext("staging"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.switchFocus(g, v, gui.getMainView()); err != nil {
|
||||
if err := gui.switchFocus(gui.g, gui.getFilesView(), gui.getMainView()); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.refreshStagingPanel()
|
||||
return gui.refreshStagingPanel(forceSecondaryFocused, selectedLineIdx)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleFilePress(g *gocui.Gui, v *gocui.View) error {
|
||||
@@ -240,54 +269,6 @@ func (gui *Gui) handleStageAll(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.handleFileSelect(g, v, false)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleAddPatch(g *gocui.Gui, v *gocui.View) error {
|
||||
file, err := gui.getSelectedFile(g)
|
||||
if err != nil {
|
||||
if err == gui.Errors.ErrNoFiles {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if !file.HasUnstagedChanges {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("FileHasNoUnstagedChanges"))
|
||||
}
|
||||
if !file.Tracked {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("CannotGitAdd"))
|
||||
}
|
||||
|
||||
gui.SubProcess = gui.GitCommand.AddPatch(file.Name)
|
||||
return gui.Errors.ErrSubProcess
|
||||
}
|
||||
|
||||
func (gui *Gui) handleFileRemove(g *gocui.Gui, v *gocui.View) error {
|
||||
file, err := gui.getSelectedFile(g)
|
||||
if err != nil {
|
||||
if err == gui.Errors.ErrNoFiles {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
var deleteVerb string
|
||||
if file.Tracked {
|
||||
deleteVerb = gui.Tr.SLocalize("checkout")
|
||||
} else {
|
||||
deleteVerb = gui.Tr.SLocalize("delete")
|
||||
}
|
||||
message := gui.Tr.TemplateLocalize(
|
||||
"SureTo",
|
||||
Teml{
|
||||
"deleteVerb": deleteVerb,
|
||||
"fileName": file.Name,
|
||||
},
|
||||
)
|
||||
return gui.createConfirmationPanel(g, v, strings.Title(deleteVerb)+" file", message, func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.RemoveFile(file); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.refreshFiles()
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
|
||||
file, err := gui.getSelectedFile(g)
|
||||
if err != nil {
|
||||
@@ -302,6 +283,22 @@ func (gui *Gui) handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.refreshFiles()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleWIPCommitPress(g *gocui.Gui, filesView *gocui.View) error {
|
||||
skipHookPreifx := gui.Config.GetUserConfig().GetString("git.skipHookPrefix")
|
||||
if skipHookPreifx == "" {
|
||||
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("SkipHookPrefixNotConfigured"))
|
||||
}
|
||||
|
||||
if err := gui.renderString(g, "commitMessage", skipHookPreifx); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.getCommitMessageView().SetCursor(len(skipHookPreifx), 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.handleCommitPress(g, filesView)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
|
||||
if len(gui.stagedFiles()) == 0 && gui.State.WorkingTreeState == "normal" {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit"))
|
||||
@@ -327,10 +324,14 @@ func (gui *Gui) handleAmendCommitPress(g *gocui.Gui, filesView *gocui.View) erro
|
||||
title := strings.Title(gui.Tr.SLocalize("AmendLastCommit"))
|
||||
question := gui.Tr.SLocalize("SureToAmend")
|
||||
|
||||
return gui.createConfirmationPanel(g, filesView, title, question, func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.runSyncOrAsyncCommand(gui.GitCommand.AmendHead()); err != nil {
|
||||
return gui.createConfirmationPanel(g, filesView, true, title, question, func(g *gocui.Gui, v *gocui.View) error {
|
||||
ok, err := gui.runSyncOrAsyncCommand(gui.GitCommand.AmendHead())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(g)
|
||||
}, nil)
|
||||
@@ -355,13 +356,14 @@ func (gui *Gui) PrepareSubProcess(g *gocui.Gui, commands ...string) {
|
||||
}
|
||||
|
||||
func (gui *Gui) editFile(filename string) error {
|
||||
return gui.runSyncOrAsyncCommand(gui.OSCommand.EditFile(filename))
|
||||
_, err := gui.runSyncOrAsyncCommand(gui.OSCommand.EditFile(filename))
|
||||
return err
|
||||
}
|
||||
|
||||
func (gui *Gui) handleFileEdit(g *gocui.Gui, v *gocui.View) error {
|
||||
file, err := gui.getSelectedFile(g)
|
||||
if err != nil {
|
||||
return err
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
}
|
||||
|
||||
return gui.editFile(file.Name)
|
||||
@@ -370,7 +372,7 @@ func (gui *Gui) handleFileEdit(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleFileOpen(g *gocui.Gui, v *gocui.View) error {
|
||||
file, err := gui.getSelectedFile(g)
|
||||
if err != nil {
|
||||
return err
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
}
|
||||
return gui.openFile(file.Name)
|
||||
}
|
||||
@@ -422,14 +424,14 @@ func (gui *Gui) pullFiles(g *gocui.Gui, v *gocui.View) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) pushWithForceFlag(g *gocui.Gui, v *gocui.View, force bool) error {
|
||||
func (gui *Gui) pushWithForceFlag(g *gocui.Gui, v *gocui.View, force bool, upstream string) error {
|
||||
if err := gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("PushWait")); err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
unamePassOpend := false
|
||||
branchName := gui.State.Branches[0].Name
|
||||
err := gui.GitCommand.Push(branchName, force, func(passOrUname string) string {
|
||||
err := gui.GitCommand.Push(branchName, force, upstream, func(passOrUname string) string {
|
||||
unamePassOpend = true
|
||||
return gui.waitForPassUname(g, v, passOrUname)
|
||||
})
|
||||
@@ -441,27 +443,35 @@ func (gui *Gui) pushWithForceFlag(g *gocui.Gui, v *gocui.View, force bool) error
|
||||
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()
|
||||
if pullables == "?" || pullables == "0" {
|
||||
return gui.pushWithForceFlag(g, v, false)
|
||||
currentBranchName, err := gui.GitCommand.CurrentBranchName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err := gui.createConfirmationPanel(g, nil, gui.Tr.SLocalize("ForcePush"), gui.Tr.SLocalize("ForcePushPrompt"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.pushWithForceFlag(g, v, true)
|
||||
|
||||
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, "")
|
||||
}
|
||||
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, "")
|
||||
}, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error {
|
||||
file, err := gui.getSelectedFile(g)
|
||||
if err != nil {
|
||||
if err != gui.Errors.ErrNoFiles {
|
||||
return err
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !file.HasInlineMergeConflicts {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("FileNoMergeCons"))
|
||||
}
|
||||
if err := gui.changeContext("main", "merging"); err != nil {
|
||||
if err := gui.changeContext("merging"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.switchFocus(g, v, gui.getMainView()); err != nil {
|
||||
@@ -479,15 +489,6 @@ func (gui *Gui) handleAbortMerge(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.refreshFiles()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleResetAndClean(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("ClearFilePanel"), gui.Tr.SLocalize("SureResetHardHead"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.ResetAndClean(); err != nil {
|
||||
gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
return gui.refreshFiles()
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (gui *Gui) openFile(filename string) error {
|
||||
if err := gui.OSCommand.OpenFile(filename); err != nil {
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
@@ -504,15 +505,181 @@ func (gui *Gui) anyFilesWithMergeConflicts() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSoftReset(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("SoftReset"), gui.Tr.SLocalize("ConfirmSoftReset"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.SoftReset("HEAD^"); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
type discardOption struct {
|
||||
handler func(fileName *commands.File) error
|
||||
description string
|
||||
}
|
||||
|
||||
if err := gui.refreshCommits(gui.g); err != nil {
|
||||
type discardAllOption struct {
|
||||
handler func() error
|
||||
description string
|
||||
command string
|
||||
}
|
||||
|
||||
// GetDisplayStrings is a function.
|
||||
func (r *discardOption) GetDisplayStrings(isFocused bool) []string {
|
||||
return []string{r.description}
|
||||
}
|
||||
|
||||
// GetDisplayStrings is a function.
|
||||
func (r *discardAllOption) GetDisplayStrings(isFocused bool) []string {
|
||||
return []string{r.description, color.New(color.FgRed).Sprint(r.command)}
|
||||
}
|
||||
|
||||
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()
|
||||
}, nil)
|
||||
}
|
||||
|
||||
return gui.createMenu(file.Name, options, len(options), handleMenuPress)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateResetMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
options := []*discardAllOption{
|
||||
{
|
||||
description: gui.Tr.SLocalize("discardAllChangesToAllFiles"),
|
||||
command: "reset --hard HEAD && git clean -fd",
|
||||
handler: func() error {
|
||||
return gui.GitCommand.ResetAndClean()
|
||||
},
|
||||
},
|
||||
{
|
||||
description: gui.Tr.SLocalize("discardAnyUnstagedChanges"),
|
||||
command: "git checkout -- .",
|
||||
handler: func() error {
|
||||
return gui.GitCommand.DiscardAnyUnstagedFileChanges()
|
||||
},
|
||||
},
|
||||
{
|
||||
description: gui.Tr.SLocalize("discardUntrackedFiles"),
|
||||
command: "git clean -fd",
|
||||
handler: func() error {
|
||||
return gui.GitCommand.RemoveUntrackedFiles()
|
||||
},
|
||||
},
|
||||
{
|
||||
description: gui.Tr.SLocalize("softReset"),
|
||||
command: "git reset --soft HEAD",
|
||||
handler: func() error {
|
||||
return gui.GitCommand.ResetSoftHead()
|
||||
},
|
||||
},
|
||||
{
|
||||
description: gui.Tr.SLocalize("hardReset"),
|
||||
command: "git reset --hard HEAD",
|
||||
handler: func() error {
|
||||
return gui.GitCommand.ResetHardHead()
|
||||
},
|
||||
},
|
||||
{
|
||||
description: gui.Tr.SLocalize("cancel"),
|
||||
handler: func() error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
handleMenuPress := func(index int) error {
|
||||
if err := options[index].handler(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.refreshFiles()
|
||||
}
|
||||
|
||||
return gui.createMenu("", 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)
|
||||
gui.SubProcess = gui.OSCommand.RunCustomCommand(command)
|
||||
return gui.Errors.ErrSubProcess
|
||||
})
|
||||
}
|
||||
|
||||
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{
|
||||
{
|
||||
description: gui.Tr.SLocalize("stashAllChanges"),
|
||||
handler: func() error {
|
||||
return gui.handleStashSave(gui.GitCommand.StashSave)
|
||||
},
|
||||
},
|
||||
{
|
||||
description: gui.Tr.SLocalize("stashStagedChanges"),
|
||||
handler: 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)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStashChanges(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.handleStashSave(gui.GitCommand.StashSave)
|
||||
}
|
||||
|
||||
+393
-162
@@ -1,14 +1,15 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
// "io"
|
||||
// "io/ioutil"
|
||||
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -23,11 +24,14 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/updates"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const StartupPopupVersion = 1
|
||||
|
||||
// OverlappingEdges determines if panel edges overlap
|
||||
var OverlappingEdges = false
|
||||
|
||||
@@ -80,11 +84,14 @@ type Gui struct {
|
||||
// for now the staging panel state, unlike the other panel states, is going to be
|
||||
// non-mutative, so that we don't accidentally end up
|
||||
// with mismatches of data. We might change this in the future
|
||||
type stagingPanelState struct {
|
||||
SelectedLine int
|
||||
StageableLines []int
|
||||
HunkStarts []int
|
||||
Diff string
|
||||
type lineByLinePanelState struct {
|
||||
SelectedLineIdx int
|
||||
FirstLineIdx int
|
||||
LastLineIdx int
|
||||
Diff string
|
||||
PatchParser *commands.PatchParser
|
||||
SelectMode int // one of LINE, HUNK, or RANGE
|
||||
SecondaryFocused bool // this is for if we show the left or right panel
|
||||
}
|
||||
|
||||
type mergingPanelState struct {
|
||||
@@ -103,7 +110,8 @@ type branchPanelState struct {
|
||||
}
|
||||
|
||||
type commitPanelState struct {
|
||||
SelectedLine int
|
||||
SelectedLine int
|
||||
SpecificDiffMode bool
|
||||
}
|
||||
|
||||
type stashPanelState struct {
|
||||
@@ -112,16 +120,28 @@ type stashPanelState struct {
|
||||
|
||||
type menuPanelState struct {
|
||||
SelectedLine int
|
||||
OnPress func(g *gocui.Gui, v *gocui.View) error
|
||||
}
|
||||
|
||||
type commitFilesPanelState struct {
|
||||
SelectedLine int
|
||||
}
|
||||
|
||||
type statusPanelState struct {
|
||||
pushables string
|
||||
pullables string
|
||||
}
|
||||
|
||||
type panelStates struct {
|
||||
Files *filePanelState
|
||||
Branches *branchPanelState
|
||||
Commits *commitPanelState
|
||||
Stash *stashPanelState
|
||||
Menu *menuPanelState
|
||||
Staging *stagingPanelState
|
||||
Merging *mergingPanelState
|
||||
Files *filePanelState
|
||||
Branches *branchPanelState
|
||||
Commits *commitPanelState
|
||||
Stash *stashPanelState
|
||||
Menu *menuPanelState
|
||||
LineByLine *lineByLinePanelState
|
||||
Merging *mergingPanelState
|
||||
CommitFiles *commitFilesPanelState
|
||||
Status *statusPanelState
|
||||
}
|
||||
|
||||
type guiState struct {
|
||||
@@ -129,15 +149,22 @@ type guiState struct {
|
||||
Branches []*commands.Branch
|
||||
Commits []*commands.Commit
|
||||
StashEntries []*commands.StashEntry
|
||||
CommitFiles []*commands.CommitFile
|
||||
DiffEntries []*commands.Commit
|
||||
MenuItemCount int // can't store the actual list because it's of interface{} type
|
||||
PreviousView string
|
||||
Platform commands.Platform
|
||||
Updating bool
|
||||
Panels *panelStates
|
||||
WorkingTreeState string // one of "merging", "rebasing", "normal"
|
||||
Contexts map[string]string
|
||||
Context string // important not to set this value directly but to use gui.changeContext("new context")
|
||||
CherryPickedCommits []*commands.Commit
|
||||
SplitMainPanel bool
|
||||
RetainOriginalDir bool
|
||||
}
|
||||
|
||||
// for now the split view will always be on
|
||||
|
||||
// NewGui builds a new gui handler
|
||||
func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *commands.OSCommand, tr *i18n.Localizer, config config.AppConfigurer, updater *updates.Updater) (*Gui, error) {
|
||||
|
||||
@@ -147,19 +174,22 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *comma
|
||||
Commits: make([]*commands.Commit, 0),
|
||||
CherryPickedCommits: make([]*commands.Commit, 0),
|
||||
StashEntries: make([]*commands.StashEntry, 0),
|
||||
DiffEntries: make([]*commands.Commit, 0),
|
||||
Platform: *oSCommand.Platform,
|
||||
Panels: &panelStates{
|
||||
Files: &filePanelState{SelectedLine: -1},
|
||||
Branches: &branchPanelState{SelectedLine: 0},
|
||||
Commits: &commitPanelState{SelectedLine: -1},
|
||||
Stash: &stashPanelState{SelectedLine: -1},
|
||||
Menu: &menuPanelState{SelectedLine: 0},
|
||||
Files: &filePanelState{SelectedLine: -1},
|
||||
Branches: &branchPanelState{SelectedLine: 0},
|
||||
Commits: &commitPanelState{SelectedLine: -1},
|
||||
CommitFiles: &commitFilesPanelState{SelectedLine: -1},
|
||||
Stash: &stashPanelState{SelectedLine: -1},
|
||||
Menu: &menuPanelState{SelectedLine: 0},
|
||||
Merging: &mergingPanelState{
|
||||
ConflictIndex: 0,
|
||||
ConflictTop: true,
|
||||
Conflicts: []commands.Conflict{},
|
||||
EditHistory: stack.New(),
|
||||
},
|
||||
Status: &statusPanelState{},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -179,15 +209,15 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *comma
|
||||
return gui, nil
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollUpMain(g *gocui.Gui, v *gocui.View) error {
|
||||
mainView, _ := g.View("main")
|
||||
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) scrollDownMain(g *gocui.Gui, v *gocui.View) error {
|
||||
mainView, _ := g.View("main")
|
||||
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") {
|
||||
@@ -200,6 +230,22 @@ func (gui *Gui) scrollDownMain(g *gocui.Gui, v *gocui.View) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollUpMain(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.scrollUpView("main")
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollDownMain(g *gocui.Gui, v *gocui.View) error {
|
||||
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) handleRefresh(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.refreshSidePanels(g)
|
||||
}
|
||||
@@ -213,20 +259,21 @@ func max(a, b int) int {
|
||||
|
||||
// getFocusLayout returns a manager function for when view gain and lose focus
|
||||
func (gui *Gui) getFocusLayout() func(g *gocui.Gui) error {
|
||||
var focusedView *gocui.View
|
||||
var previousView *gocui.View
|
||||
return func(g *gocui.Gui) error {
|
||||
v := gui.g.CurrentView()
|
||||
if v != focusedView {
|
||||
if err := gui.onFocusChange(); err != nil {
|
||||
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.onFocusLost(focusedView); err != nil {
|
||||
if err := gui.onFocus(newView); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.onFocus(v); err != nil {
|
||||
return err
|
||||
}
|
||||
focusedView = v
|
||||
previousView = newView
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -237,26 +284,31 @@ func (gui *Gui) onFocusChange() error {
|
||||
for _, view := range gui.g.Views() {
|
||||
view.Highlight = view == currentView
|
||||
}
|
||||
return gui.setMainTitle()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) onFocusLost(v *gocui.View) error {
|
||||
func (gui *Gui) onFocusLost(v *gocui.View, newView *gocui.View) error {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
if v.Name() == "branches" {
|
||||
switch v.Name() {
|
||||
case "branches":
|
||||
// This stops the branches panel from showing the upstream/downstream changes to the selected branch, when it loses focus
|
||||
// inside renderListPanel it checks to see if the panel has focus
|
||||
if err := gui.renderListPanel(gui.getBranchesView(), gui.State.Branches); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if v.Name() == "main" {
|
||||
// if we have lost focus to a popup panel, that's okay
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := gui.changeContext("main", "normal"); err != nil {
|
||||
case "main":
|
||||
// if we have lost focus to a first-class panel, we need to do some cleanup
|
||||
if err := gui.changeContext("normal"); err != nil {
|
||||
return err
|
||||
}
|
||||
case "commitFiles":
|
||||
if gui.State.Context != "patch-building" {
|
||||
if _, err := gui.g.SetViewOnBottom(v.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
gui.Log.Info(v.Name() + " focus lost")
|
||||
return nil
|
||||
@@ -274,20 +326,74 @@ func (gui *Gui) onFocus(v *gocui.View) error {
|
||||
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
|
||||
}
|
||||
leftSideWidth := width / 3
|
||||
statusFilesBoundary := 2
|
||||
filesBranchesBoundary := 2 * height / 5
|
||||
commitsBranchesBoundary := 3 * height / 5
|
||||
optionsTop := height - 2
|
||||
commitsStashBoundary := optionsTop - 3
|
||||
optionsVersionBoundary := width - max(len(utils.Decolorise(information)), 1)
|
||||
minimumHeight := 18
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
currView := gui.g.CurrentView()
|
||||
currentCyclebleView := gui.State.PreviousView
|
||||
if currView != nil {
|
||||
viewName := currView.Name()
|
||||
usePreviouseView := true
|
||||
for _, view := range cyclableViews {
|
||||
if view == viewName {
|
||||
currentCyclebleView = viewName
|
||||
usePreviouseView = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if usePreviouseView {
|
||||
currentCyclebleView = gui.State.PreviousView
|
||||
}
|
||||
}
|
||||
|
||||
usableSpace := height - 7
|
||||
extraSpace := usableSpace - (usableSpace/3)*3
|
||||
|
||||
vHeights := map[string]int{
|
||||
"status": 3,
|
||||
"files": (usableSpace / 3) + extraSpace,
|
||||
"branches": usableSpace / 3,
|
||||
"commits": usableSpace / 3,
|
||||
"stash": 3,
|
||||
"options": 1,
|
||||
}
|
||||
|
||||
if height < 28 {
|
||||
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
|
||||
}
|
||||
|
||||
optionsVersionBoundary := width - max(len(utils.Decolorise(information)), 1)
|
||||
|
||||
appStatus := gui.statusManager.getStatusString()
|
||||
appStatusOptionsBoundary := 0
|
||||
@@ -300,103 +406,128 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
||||
panelSpacing = 0
|
||||
}
|
||||
|
||||
if height < minimumHeight || width < minimumWidth {
|
||||
v, err := g.SetView("limit", 0, 0, max(width-1, 2), max(height-1, 2), 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
|
||||
}
|
||||
_, _ = g.SetViewOnBottom("limit")
|
||||
g.DeleteView("limit")
|
||||
|
||||
v, err := g.SetView("main", leftSideWidth+panelSpacing, 0, width-1, optionsTop, gocui.LEFT)
|
||||
textColor := theme.GocuiDefaultTextColor
|
||||
leftSideWidth := width / 3
|
||||
panelSplitX := width - 1
|
||||
if gui.State.SplitMainPanel {
|
||||
units := 7
|
||||
leftSideWidth = width / units
|
||||
panelSplitX = (1 + ((units - 1) / 2)) * width / units
|
||||
}
|
||||
|
||||
main := "main"
|
||||
secondary := "secondary"
|
||||
swappingMainPanels := gui.State.Panels.LineByLine != nil && gui.State.Panels.LineByLine.SecondaryFocused
|
||||
if swappingMainPanels {
|
||||
main = "secondary"
|
||||
secondary = "main"
|
||||
}
|
||||
|
||||
v, err := g.SetView(main, leftSideWidth+panelSpacing, 0, panelSplitX, height-2, gocui.LEFT)
|
||||
if err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
v.Title = gui.Tr.SLocalize("DiffTitle")
|
||||
v.Wrap = true
|
||||
v.FgColor = gocui.ColorWhite
|
||||
v.FgColor = textColor
|
||||
}
|
||||
|
||||
if v, err := g.SetView("status", 0, 0, leftSideWidth, statusFilesBoundary, gocui.BOTTOM|gocui.RIGHT); err != nil {
|
||||
hiddenViewOffset := 0
|
||||
if !gui.State.SplitMainPanel {
|
||||
hiddenViewOffset = 9999
|
||||
}
|
||||
secondaryView, err := g.SetView(secondary, panelSplitX+1+hiddenViewOffset, hiddenViewOffset, width-1+hiddenViewOffset, height-2+hiddenViewOffset, 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
|
||||
}
|
||||
|
||||
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 = gocui.ColorWhite
|
||||
v.FgColor = textColor
|
||||
}
|
||||
|
||||
filesView, err := g.SetView("files", 0, statusFilesBoundary+panelSpacing, leftSideWidth, filesBranchesBoundary, gocui.TOP|gocui.BOTTOM)
|
||||
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")
|
||||
v.FgColor = gocui.ColorWhite
|
||||
v.FgColor = textColor
|
||||
}
|
||||
|
||||
branchesView, err := g.SetView("branches", 0, filesBranchesBoundary+panelSpacing, leftSideWidth, commitsBranchesBoundary, gocui.TOP|gocui.BOTTOM)
|
||||
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.FgColor = gocui.ColorWhite
|
||||
branchesView.FgColor = textColor
|
||||
}
|
||||
|
||||
commitsView, err := g.SetView("commits", 0, commitsBranchesBoundary+panelSpacing, leftSideWidth, commitsStashBoundary, gocui.TOP|gocui.BOTTOM)
|
||||
if v, err := g.SetViewBeneath("commitFiles", "branches", vHeights["commits"]); err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
v.Title = gui.Tr.SLocalize("CommitFiles")
|
||||
v.FgColor = textColor
|
||||
}
|
||||
|
||||
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.FgColor = gocui.ColorWhite
|
||||
commitsView.FgColor = textColor
|
||||
}
|
||||
|
||||
stashView, err := g.SetView("stash", 0, commitsStashBoundary+panelSpacing, leftSideWidth, optionsTop, gocui.TOP|gocui.RIGHT)
|
||||
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 = gocui.ColorWhite
|
||||
stashView.FgColor = textColor
|
||||
}
|
||||
|
||||
if v, err := g.SetView("options", appStatusOptionsBoundary-1, optionsTop, optionsVersionBoundary-1, optionsTop+2, 0); err != nil {
|
||||
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
|
||||
if v.FgColor, err = gui.GetOptionsPanelTextColor(); err != nil {
|
||||
return err
|
||||
}
|
||||
userConfig := gui.Config.GetUserConfig()
|
||||
v.FgColor = theme.GetColor(userConfig.GetStringSlice("gui.theme.optionsTextColor"))
|
||||
}
|
||||
|
||||
if gui.getCommitMessageView() == nil {
|
||||
// doesn't matter where this view starts because it will be hidden
|
||||
if commitMessageView, err := g.SetView("commitMessage", 0, 0, width/2, height/2, 0); err != nil {
|
||||
if commitMessageView, err := g.SetView("commitMessage", width, height, width*2, height*2, 0); err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
g.SetViewOnBottom("commitMessage")
|
||||
commitMessageView.Title = gui.Tr.SLocalize("CommitMessage")
|
||||
commitMessageView.FgColor = gocui.ColorWhite
|
||||
commitMessageView.FgColor = textColor
|
||||
commitMessageView.Editable = true
|
||||
commitMessageView.Editor = gocui.EditorFunc(gui.simpleEditor)
|
||||
}
|
||||
}
|
||||
|
||||
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", 0, 0, width/2, height/2, 0); err != nil {
|
||||
if credentialsView, err := g.SetView("credentials", width, height, width*2, height*2, 0); err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
@@ -405,13 +536,12 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
||||
return err
|
||||
}
|
||||
credentialsView.Title = gui.Tr.SLocalize("CredentialsUsername")
|
||||
credentialsView.FgColor = gocui.ColorWhite
|
||||
credentialsView.FgColor = textColor
|
||||
credentialsView.Editable = true
|
||||
credentialsView.Editor = gocui.EditorFunc(gui.simpleEditor)
|
||||
}
|
||||
}
|
||||
|
||||
if appStatusView, err := g.SetView("appStatus", -1, optionsTop, width, optionsTop+2, 0); err != nil {
|
||||
if appStatusView, err := g.SetView("appStatus", -1, height-2, width, height, 0); err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
@@ -423,7 +553,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
||||
}
|
||||
}
|
||||
|
||||
if v, err := g.SetView("information", optionsVersionBoundary-1, optionsTop, width, optionsTop+2, 0); err != nil {
|
||||
if v, err := g.SetView("information", optionsVersionBoundary-1, height-2, width, height, 0); err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
@@ -434,47 +564,41 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// these are only called once (it's a place to put all the things you want
|
||||
// to happen on startup after the screen is first rendered)
|
||||
gui.Updater.CheckForNewUpdate(gui.onBackgroundUpdateCheckFinish, false)
|
||||
if err := gui.updateRecentRepoList(); err != nil {
|
||||
// doing this here because it'll only happen once
|
||||
if err := gui.loadNewRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
gui.waitForIntro.Done()
|
||||
|
||||
if _, err := gui.g.SetCurrentView(filesView.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.refreshSidePanels(gui.g); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.switchFocus(g, nil, filesView); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if gui.Config.GetUserConfig().GetString("reporting") == "undetermined" {
|
||||
if err := gui.promptAnonymousReporting(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
listViews := map[*gocui.View]int{
|
||||
filesView: gui.State.Panels.Files.SelectedLine,
|
||||
branchesView: gui.State.Panels.Branches.SelectedLine,
|
||||
commitsView: gui.State.Panels.Commits.SelectedLine,
|
||||
stashView: gui.State.Panels.Stash.SelectedLine,
|
||||
if gui.g.CurrentView() == nil {
|
||||
if _, err := gui.g.SetCurrentView(gui.getFilesView().Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.switchFocus(gui.g, nil, gui.getFilesView()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
type listViewState struct {
|
||||
selectedLine int
|
||||
lineCount int
|
||||
}
|
||||
|
||||
listViews := map[*gocui.View]listViewState{
|
||||
filesView: {selectedLine: gui.State.Panels.Files.SelectedLine, lineCount: len(gui.State.Files)},
|
||||
branchesView: {selectedLine: gui.State.Panels.Branches.SelectedLine, lineCount: len(gui.State.Branches)},
|
||||
commitsView: {selectedLine: gui.State.Panels.Commits.SelectedLine, lineCount: len(gui.State.Commits)},
|
||||
stashView: {selectedLine: gui.State.Panels.Stash.SelectedLine, lineCount: len(gui.State.StashEntries)},
|
||||
}
|
||||
|
||||
// menu view might not exist so we check to be safe
|
||||
if menuView, err := gui.g.View("menu"); err == nil {
|
||||
listViews[menuView] = gui.State.Panels.Menu.SelectedLine
|
||||
listViews[menuView] = listViewState{selectedLine: gui.State.Panels.Menu.SelectedLine, lineCount: gui.State.MenuItemCount}
|
||||
}
|
||||
for view, selectedLine := range listViews {
|
||||
for view, state := range listViews {
|
||||
// check if the selected line is now out of view and if so refocus it
|
||||
if err := gui.focusPoint(0, selectedLine, view); err != nil {
|
||||
if err := gui.focusPoint(0, state.selectedLine, state.lineCount, view); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -486,12 +610,53 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
||||
return gui.resizeCurrentPopupPanel(g)
|
||||
}
|
||||
|
||||
func (gui *Gui) promptAnonymousReporting() error {
|
||||
return gui.createConfirmationPanel(gui.g, nil, gui.Tr.SLocalize("AnonymousReportingTitle"), gui.Tr.SLocalize("AnonymousReportingPrompt"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.waitForIntro.Done()
|
||||
func (gui *Gui) loadNewRepo() error {
|
||||
gui.Updater.CheckForNewUpdate(gui.onBackgroundUpdateCheckFinish, false)
|
||||
if err := gui.updateRecentRepoList(); err != nil {
|
||||
return err
|
||||
}
|
||||
gui.waitForIntro.Done()
|
||||
|
||||
if err := gui.refreshSidePanels(gui.g); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) showInitialPopups(tasks []func(chan struct{}) error) {
|
||||
gui.waitForIntro.Add(len(tasks))
|
||||
done := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
for _, task := range tasks {
|
||||
go func() {
|
||||
if err := task(done); err != nil {
|
||||
_ = gui.createErrorPanel(gui.g, err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
<-done
|
||||
gui.waitForIntro.Done()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (gui *Gui) showShamelessSelfPromotionMessage(done chan struct{}) error {
|
||||
onConfirm := func(g *gocui.Gui, v *gocui.View) error {
|
||||
done <- struct{}{}
|
||||
return gui.Config.WriteToUserConfig("startupPopupVersion", StartupPopupVersion)
|
||||
}
|
||||
|
||||
return gui.createConfirmationPanel(gui.g, nil, true, gui.Tr.SLocalize("ShamelessSelfPromotionTitle"), gui.Tr.SLocalize("ShamelessSelfPromotionMessage"), onConfirm, onConfirm)
|
||||
}
|
||||
|
||||
func (gui *Gui) promptAnonymousReporting(done chan struct{}) error {
|
||||
return gui.createConfirmationPanel(gui.g, nil, true, gui.Tr.SLocalize("AnonymousReportingTitle"), gui.Tr.SLocalize("AnonymousReportingPrompt"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
done <- struct{}{}
|
||||
return gui.Config.WriteToUserConfig("reporting", "on")
|
||||
}, func(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.waitForIntro.Done()
|
||||
done <- struct{}{}
|
||||
return gui.Config.WriteToUserConfig("reporting", "off")
|
||||
})
|
||||
}
|
||||
@@ -509,7 +674,7 @@ func (gui *Gui) fetch(g *gocui.Gui, v *gocui.View, canAskForCredentials bool) (u
|
||||
close := func(g *gocui.Gui, v *gocui.View) error {
|
||||
return nil
|
||||
}
|
||||
_ = gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("Error"), coloredMessage, close, close)
|
||||
_ = gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("Error"), coloredMessage, close, close)
|
||||
}
|
||||
|
||||
gui.refreshStatus(g)
|
||||
@@ -530,6 +695,7 @@ func (gui *Gui) renderGlobalOptions() error {
|
||||
"← → ↑ ↓": gui.Tr.SLocalize("navigate"),
|
||||
"esc/q": gui.Tr.SLocalize("close"),
|
||||
"x": gui.Tr.SLocalize("menu"),
|
||||
"1-5": gui.Tr.SLocalize("jump"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -541,6 +707,23 @@ func (gui *Gui) goEvery(interval time.Duration, function func() error) {
|
||||
}()
|
||||
}
|
||||
|
||||
func (gui *Gui) startBackgroundFetch() {
|
||||
gui.waitForIntro.Wait()
|
||||
isNew := gui.Config.GetIsNewRepo()
|
||||
if !isNew {
|
||||
time.After(60 * time.Second)
|
||||
}
|
||||
_, err := gui.fetch(gui.g, gui.g.CurrentView(), false)
|
||||
if err != nil && strings.Contains(err.Error(), "exit status 128") && isNew {
|
||||
_ = gui.createConfirmationPanel(gui.g, gui.g.CurrentView(), true, gui.Tr.SLocalize("NoAutomaticGitFetchTitle"), gui.Tr.SLocalize("NoAutomaticGitFetchBody"), nil, nil)
|
||||
} else {
|
||||
gui.goEvery(time.Second*60, func() error {
|
||||
_, err := gui.fetch(gui.g, gui.g.CurrentView(), false)
|
||||
return err
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Run setup the gui with keybindings and start the mainloop
|
||||
func (gui *Gui) Run() error {
|
||||
g, err := gocui.NewGui(gocui.OutputNormal, OverlappingEdges)
|
||||
@@ -555,32 +738,26 @@ func (gui *Gui) Run() error {
|
||||
|
||||
gui.g = g // TODO: always use gui.g rather than passing g around everywhere
|
||||
|
||||
if err := gui.SetColorScheme(); err != nil {
|
||||
if err := gui.setColorScheme(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
popupTasks := []func(chan struct{}) error{}
|
||||
if gui.Config.GetUserConfig().GetString("reporting") == "undetermined" {
|
||||
gui.waitForIntro.Add(2)
|
||||
} else {
|
||||
gui.waitForIntro.Add(1)
|
||||
popupTasks = append(popupTasks, gui.promptAnonymousReporting)
|
||||
}
|
||||
configPopupVersion := gui.Config.GetUserConfig().GetInt("StartupPopupVersion")
|
||||
// -1 means we've disabled these popups
|
||||
if configPopupVersion != -1 && configPopupVersion < StartupPopupVersion {
|
||||
popupTasks = append(popupTasks, gui.showShamelessSelfPromotionMessage)
|
||||
}
|
||||
gui.showInitialPopups(popupTasks)
|
||||
|
||||
gui.waitForIntro.Add(1)
|
||||
if gui.Config.GetUserConfig().GetBool("git.autoFetch") {
|
||||
go gui.startBackgroundFetch()
|
||||
}
|
||||
|
||||
go func() {
|
||||
gui.waitForIntro.Wait()
|
||||
isNew := gui.Config.GetIsNewRepo()
|
||||
if !isNew {
|
||||
time.After(60 * time.Second)
|
||||
}
|
||||
_, err := gui.fetch(g, g.CurrentView(), false)
|
||||
if err != nil && strings.Contains(err.Error(), "exit status 128") && isNew {
|
||||
_ = gui.createConfirmationPanel(g, g.CurrentView(), gui.Tr.SLocalize("NoAutomaticGitFetchTitle"), gui.Tr.SLocalize("NoAutomaticGitFetchBody"), nil, nil)
|
||||
} else {
|
||||
gui.goEvery(time.Second*60, func() error {
|
||||
_, err := gui.fetch(gui.g, gui.g.CurrentView(), false)
|
||||
return err
|
||||
})
|
||||
}
|
||||
}()
|
||||
gui.goEvery(time.Second*10, gui.refreshFiles)
|
||||
gui.goEvery(time.Millisecond*50, gui.renderAppStatus)
|
||||
|
||||
@@ -590,6 +767,8 @@ func (gui *Gui) Run() error {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.Log.Warn("starting main loop")
|
||||
|
||||
err = g.MainLoop()
|
||||
return err
|
||||
}
|
||||
@@ -601,18 +780,19 @@ func (gui *Gui) RunWithSubprocesses() error {
|
||||
for {
|
||||
if err := gui.Run(); err != nil {
|
||||
if err == gocui.ErrQuit {
|
||||
if !gui.State.RetainOriginalDir {
|
||||
if err := gui.recordCurrentDirectory(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
} else if err == gui.Errors.ErrSwitchRepo {
|
||||
continue
|
||||
} else if err == gui.Errors.ErrSubProcess {
|
||||
gui.SubProcess.Stdin = os.Stdin
|
||||
gui.SubProcess.Stdout = os.Stdout
|
||||
gui.SubProcess.Stderr = os.Stderr
|
||||
gui.SubProcess.Run()
|
||||
gui.SubProcess.Stdout = ioutil.Discard
|
||||
gui.SubProcess.Stderr = ioutil.Discard
|
||||
gui.SubProcess.Stdin = nil
|
||||
gui.SubProcess = nil
|
||||
if err := gui.runCommand(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
@@ -621,16 +801,28 @@ func (gui *Gui) RunWithSubprocesses() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) quit(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.State.Updating {
|
||||
return gui.createUpdateQuitConfirmation(g, v)
|
||||
func (gui *Gui) runCommand() error {
|
||||
gui.SubProcess.Stdout = os.Stdout
|
||||
gui.SubProcess.Stderr = os.Stdout
|
||||
gui.SubProcess.Stdin = os.Stdin
|
||||
|
||||
fmt.Fprintf(os.Stdout, "\n%s\n\n", utils.ColoredString("+ "+strings.Join(gui.SubProcess.Args, " "), color.FgBlue))
|
||||
|
||||
if err := gui.SubProcess.Run(); err != nil {
|
||||
// not handling the error explicitly because usually we're going to see it
|
||||
// in the output anyway
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
if gui.Config.GetUserConfig().GetBool("confirmOnQuit") {
|
||||
return gui.createConfirmationPanel(g, v, "", gui.Tr.SLocalize("ConfirmQuit"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gocui.ErrQuit
|
||||
}, nil)
|
||||
}
|
||||
return gocui.ErrQuit
|
||||
|
||||
gui.SubProcess.Stdout = ioutil.Discard
|
||||
gui.SubProcess.Stderr = ioutil.Discard
|
||||
gui.SubProcess.Stdin = nil
|
||||
gui.SubProcess = nil
|
||||
|
||||
fmt.Fprintf(os.Stdout, "\n%s", utils.ColoredString(gui.Tr.SLocalize("pressEnterToReturn"), color.FgGreen))
|
||||
fmt.Scanln() // wait for enter press
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleDonate(g *gocui.Gui, v *gocui.View) error {
|
||||
@@ -642,5 +834,44 @@ func (gui *Gui) handleDonate(g *gocui.Gui, v *gocui.View) error {
|
||||
if cx > len(gui.Tr.SLocalize("Donate")) {
|
||||
return nil
|
||||
}
|
||||
return gui.OSCommand.OpenLink("https://donorbox.org/lazygit")
|
||||
return gui.OSCommand.OpenLink("https://github.com/sponsors/jesseduffield")
|
||||
}
|
||||
|
||||
// setColorScheme sets the color scheme for the app based on the user config
|
||||
func (gui *Gui) setColorScheme() error {
|
||||
userConfig := gui.Config.GetUserConfig()
|
||||
theme.UpdateTheme(userConfig)
|
||||
|
||||
gui.g.FgColor = theme.InactiveBorderColor
|
||||
gui.g.SelFgColor = theme.ActiveBorderColor
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
+490
-210
@@ -13,6 +13,7 @@ type Binding struct {
|
||||
Key interface{} // FIXME: find out how to get `gocui.Key | rune`
|
||||
Modifier gocui.Modifier
|
||||
Description string
|
||||
Alternative string
|
||||
}
|
||||
|
||||
// GetDisplayStrings returns the display string of a file
|
||||
@@ -28,6 +29,12 @@ func (b *Binding) GetKey() string {
|
||||
case rune:
|
||||
key = int(b.Key.(rune))
|
||||
case gocui.Key:
|
||||
if b.Key.(gocui.Key) == gocui.KeyCtrlJ {
|
||||
return "ctrl+j"
|
||||
}
|
||||
if b.Key.(gocui.Key) == gocui.KeyCtrlK {
|
||||
return "ctrl+k"
|
||||
}
|
||||
key = int(b.Key.(gocui.Key))
|
||||
}
|
||||
|
||||
@@ -51,6 +58,8 @@ func (b *Binding) GetKey() string {
|
||||
return "PgUp"
|
||||
case 65507:
|
||||
return "PgDn"
|
||||
case 9:
|
||||
return "tab"
|
||||
}
|
||||
|
||||
return string(key)
|
||||
@@ -63,25 +72,42 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
ViewName: "",
|
||||
Key: 'q',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.quit,
|
||||
Handler: gui.handleQuit,
|
||||
}, {
|
||||
ViewName: "",
|
||||
Key: 'Q',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleQuitWithoutChangingDirectory,
|
||||
}, {
|
||||
ViewName: "",
|
||||
Key: gocui.KeyCtrlC,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.quit,
|
||||
Handler: gui.handleQuit,
|
||||
}, {
|
||||
ViewName: "",
|
||||
Key: gocui.KeyEsc,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.quit,
|
||||
Handler: gui.handleQuit,
|
||||
}, {
|
||||
ViewName: "",
|
||||
Key: gocui.KeyPgup,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.scrollUpMain,
|
||||
Alternative: "fn+up",
|
||||
}, {
|
||||
ViewName: "",
|
||||
Key: gocui.KeyPgdn,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.scrollDownMain,
|
||||
Alternative: "fn+down",
|
||||
}, {
|
||||
ViewName: "",
|
||||
Key: gocui.KeyPgup,
|
||||
Key: 'K',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.scrollUpMain,
|
||||
}, {
|
||||
ViewName: "",
|
||||
Key: gocui.KeyPgdn,
|
||||
Key: 'J',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.scrollDownMain,
|
||||
}, {
|
||||
@@ -123,6 +149,21 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
Key: 'x',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleCreateOptionsMenu,
|
||||
}, {
|
||||
ViewName: "",
|
||||
Key: '?',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleCreateOptionsMenu,
|
||||
}, {
|
||||
ViewName: "",
|
||||
Key: gocui.MouseMiddle,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleCreateOptionsMenu,
|
||||
}, {
|
||||
ViewName: "",
|
||||
Key: gocui.KeyCtrlP,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleCreatePatchOptionsMenu,
|
||||
}, {
|
||||
ViewName: "status",
|
||||
Key: 'e',
|
||||
@@ -154,6 +195,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleCommitPress,
|
||||
Description: gui.Tr.SLocalize("CommitChanges"),
|
||||
},
|
||||
{
|
||||
ViewName: "files",
|
||||
Key: 'w',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleWIPCommitPress,
|
||||
Description: gui.Tr.SLocalize("commitChangesWithoutHook"),
|
||||
}, {
|
||||
ViewName: "files",
|
||||
Key: 'A',
|
||||
@@ -176,8 +224,8 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
ViewName: "files",
|
||||
Key: 'd',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleFileRemove,
|
||||
Description: gui.Tr.SLocalize("removeFile"),
|
||||
Handler: gui.handleCreateDiscardMenu,
|
||||
Description: gui.Tr.SLocalize("viewDiscardOptions"),
|
||||
}, {
|
||||
ViewName: "files",
|
||||
Key: 'e',
|
||||
@@ -202,36 +250,30 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleRefreshFiles,
|
||||
Description: gui.Tr.SLocalize("refreshFiles"),
|
||||
}, {
|
||||
ViewName: "files",
|
||||
Key: 'S',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStashSave,
|
||||
Description: gui.Tr.SLocalize("stashFiles"),
|
||||
}, {
|
||||
ViewName: "files",
|
||||
Key: 's',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSoftReset,
|
||||
Description: gui.Tr.SLocalize("softReset"),
|
||||
Handler: gui.handleStashChanges,
|
||||
Description: gui.Tr.SLocalize("stashAllChanges"),
|
||||
}, {
|
||||
ViewName: "files",
|
||||
Key: 'S',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleCreateStashMenu,
|
||||
Description: gui.Tr.SLocalize("viewStashOptions"),
|
||||
}, {
|
||||
ViewName: "files",
|
||||
Key: 'a',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStageAll,
|
||||
Description: gui.Tr.SLocalize("toggleStagedAll"),
|
||||
}, {
|
||||
ViewName: "files",
|
||||
Key: 't',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleAddPatch,
|
||||
Description: gui.Tr.SLocalize("addPatch"),
|
||||
}, {
|
||||
ViewName: "files",
|
||||
Key: 'D',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleResetAndClean,
|
||||
Description: gui.Tr.SLocalize("resetHard"),
|
||||
Handler: gui.handleCreateResetMenu,
|
||||
Description: gui.Tr.SLocalize("viewResetOptions"),
|
||||
}, {
|
||||
ViewName: "files",
|
||||
Key: gocui.KeyEnter,
|
||||
@@ -244,6 +286,12 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleGitFetch,
|
||||
Description: gui.Tr.SLocalize("fetch"),
|
||||
}, {
|
||||
ViewName: "files",
|
||||
Key: 'X',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleCustomCommand,
|
||||
Description: gui.Tr.SLocalize("executeCustomCommand"),
|
||||
}, {
|
||||
ViewName: "branches",
|
||||
Key: gocui.KeySpace,
|
||||
@@ -320,7 +368,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
ViewName: "commits",
|
||||
Key: 'g',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleResetToCommit,
|
||||
Handler: gui.handleCreateCommitResetMenu,
|
||||
Description: gui.Tr.SLocalize("resetToThisCommit"),
|
||||
}, {
|
||||
ViewName: "commits",
|
||||
@@ -328,6 +376,18 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleCommitFixup,
|
||||
Description: gui.Tr.SLocalize("fixupCommit"),
|
||||
}, {
|
||||
ViewName: "commits",
|
||||
Key: 'F',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleCreateFixupCommit,
|
||||
Description: gui.Tr.SLocalize("createFixupCommit"),
|
||||
}, {
|
||||
ViewName: "commits",
|
||||
Key: 'S',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSquashAllAboveFixupCommits,
|
||||
Description: gui.Tr.SLocalize("squashAboveCommits"),
|
||||
}, {
|
||||
ViewName: "commits",
|
||||
Key: 'd',
|
||||
@@ -336,13 +396,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
Description: gui.Tr.SLocalize("deleteCommit"),
|
||||
}, {
|
||||
ViewName: "commits",
|
||||
Key: 'J',
|
||||
Key: gocui.KeyCtrlJ,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleCommitMoveDown,
|
||||
Description: gui.Tr.SLocalize("moveDownCommit"),
|
||||
}, {
|
||||
ViewName: "commits",
|
||||
Key: 'K',
|
||||
Key: gocui.KeyCtrlK,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleCommitMoveUp,
|
||||
Description: gui.Tr.SLocalize("moveUpCommit"),
|
||||
@@ -388,6 +448,18 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.HandlePasteCommits,
|
||||
Description: gui.Tr.SLocalize("pasteCommits"),
|
||||
}, {
|
||||
ViewName: "commits",
|
||||
Key: gocui.KeyEnter,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSwitchToCommitFilesPanel,
|
||||
Description: gui.Tr.SLocalize("viewCommitFiles"),
|
||||
}, {
|
||||
ViewName: "commits",
|
||||
Key: gocui.KeySpace,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleToggleDiffCommit,
|
||||
Description: gui.Tr.SLocalize("CommitsDiff"),
|
||||
}, {
|
||||
ViewName: "stash",
|
||||
Key: gocui.KeySpace,
|
||||
@@ -441,10 +513,61 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
Key: gocui.MouseLeft,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleDonate,
|
||||
}, {
|
||||
ViewName: "commitFiles",
|
||||
Key: gocui.KeyEsc,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSwitchToCommitsPanel,
|
||||
Description: gui.Tr.SLocalize("goBack"),
|
||||
}, {
|
||||
ViewName: "commitFiles",
|
||||
Key: 'c',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleCheckoutCommitFile,
|
||||
Description: gui.Tr.SLocalize("checkoutCommitFile"),
|
||||
}, {
|
||||
ViewName: "commitFiles",
|
||||
Key: 'd',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleDiscardOldFileChange,
|
||||
Description: gui.Tr.SLocalize("discardOldFileChange"),
|
||||
},
|
||||
{
|
||||
ViewName: "commitFiles",
|
||||
Key: 'o',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleOpenOldCommitFile,
|
||||
Description: gui.Tr.SLocalize("openFile"),
|
||||
},
|
||||
{
|
||||
ViewName: "commitFiles",
|
||||
Key: gocui.KeySpace,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleToggleFileForPatch,
|
||||
Description: gui.Tr.SLocalize("toggleAddToPatch"),
|
||||
},
|
||||
{
|
||||
ViewName: "commitFiles",
|
||||
Key: gocui.KeyEnter,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleEnterCommitFile,
|
||||
Description: gui.Tr.SLocalize("enterFile"),
|
||||
},
|
||||
{
|
||||
ViewName: "secondary",
|
||||
Key: gocui.MouseWheelUp,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.scrollUpSecondary,
|
||||
},
|
||||
{
|
||||
ViewName: "secondary",
|
||||
Key: gocui.MouseWheelDown,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.scrollDownSecondary,
|
||||
},
|
||||
}
|
||||
|
||||
for _, viewName := range []string{"status", "branches", "files", "commits", "stash", "menu"} {
|
||||
for _, viewName := range []string{"status", "branches", "files", "commits", "commitFiles", "stash", "menu"} {
|
||||
bindings = append(bindings, []*Binding{
|
||||
{ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.nextView},
|
||||
{ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.previousView},
|
||||
@@ -454,17 +577,23 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
}...)
|
||||
}
|
||||
|
||||
// Appends keybindings to jump to a particular sideView using numbers
|
||||
for i, viewName := range []string{"status", "files", "branches", "commits", "stash"} {
|
||||
bindings = append(bindings, &Binding{ViewName: "", Key: rune(i+1) + '0', Modifier: gocui.ModNone, Handler: gui.goToSideView(viewName)})
|
||||
}
|
||||
|
||||
listPanelMap := map[string]struct {
|
||||
prevLine func(*gocui.Gui, *gocui.View) error
|
||||
nextLine func(*gocui.Gui, *gocui.View) error
|
||||
focus func(*gocui.Gui, *gocui.View) error
|
||||
onClick func(*gocui.Gui, *gocui.View) error
|
||||
}{
|
||||
"menu": {prevLine: gui.handleMenuPrevLine, nextLine: gui.handleMenuNextLine, focus: gui.handleMenuSelect},
|
||||
"files": {prevLine: gui.handleFilesPrevLine, nextLine: gui.handleFilesNextLine, focus: gui.handleFilesFocus},
|
||||
"branches": {prevLine: gui.handleBranchesPrevLine, nextLine: gui.handleBranchesNextLine, focus: gui.handleBranchSelect},
|
||||
"commits": {prevLine: gui.handleCommitsPrevLine, nextLine: gui.handleCommitsNextLine, focus: gui.handleCommitSelect},
|
||||
"stash": {prevLine: gui.handleStashPrevLine, nextLine: gui.handleStashNextLine, focus: gui.handleStashEntrySelect},
|
||||
"status": {focus: gui.handleStatusSelect},
|
||||
"menu": {prevLine: gui.handleMenuPrevLine, nextLine: gui.handleMenuNextLine, onClick: gui.handleMenuClick},
|
||||
"files": {prevLine: gui.handleFilesPrevLine, nextLine: gui.handleFilesNextLine, onClick: gui.handleFilesClick},
|
||||
"branches": {prevLine: gui.handleBranchesPrevLine, nextLine: gui.handleBranchesNextLine, onClick: gui.handleBranchesClick},
|
||||
"commits": {prevLine: gui.handleCommitsPrevLine, nextLine: gui.handleCommitsNextLine, onClick: gui.handleCommitsClick},
|
||||
"stash": {prevLine: gui.handleStashPrevLine, nextLine: gui.handleStashNextLine, onClick: gui.handleStashEntrySelect},
|
||||
"status": {onClick: gui.handleStatusClick},
|
||||
"commitFiles": {prevLine: gui.handleCommitFilesPrevLine, nextLine: gui.handleCommitFilesNextLine, onClick: gui.handleCommitFilesClick},
|
||||
}
|
||||
|
||||
for viewName, functions := range listPanelMap {
|
||||
@@ -475,7 +604,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
{ViewName: viewName, Key: 'j', Modifier: gocui.ModNone, Handler: functions.nextLine},
|
||||
{ViewName: viewName, Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: functions.nextLine},
|
||||
{ViewName: viewName, Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, Handler: functions.nextLine},
|
||||
{ViewName: viewName, Key: gocui.MouseLeft, Modifier: gocui.ModNone, Handler: functions.focus},
|
||||
{ViewName: viewName, Key: gocui.MouseLeft, Modifier: gocui.ModNone, Handler: functions.onClick},
|
||||
}...)
|
||||
}
|
||||
|
||||
@@ -485,9 +614,8 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
// GetCurrentKeybindings gets the list of keybindings given the current context
|
||||
func (gui *Gui) GetCurrentKeybindings() []*Binding {
|
||||
bindings := gui.GetInitialKeybindings()
|
||||
viewName := gui.currentViewName()
|
||||
currentContext := gui.State.Contexts[viewName]
|
||||
contextBindings := gui.GetContextMap()[viewName][currentContext]
|
||||
currentContext := gui.State.Context
|
||||
contextBindings := gui.GetContextMap()[currentContext]
|
||||
|
||||
return append(bindings, contextBindings...)
|
||||
}
|
||||
@@ -500,187 +628,339 @@ func (gui *Gui) keybindings(g *gocui.Gui) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := gui.setInitialContexts(); err != nil {
|
||||
if err := gui.setInitialContext(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) GetContextMap() map[string]map[string][]*Binding {
|
||||
return map[string]map[string][]*Binding{
|
||||
"main": {
|
||||
"normal": {
|
||||
{
|
||||
ViewName: "main",
|
||||
Key: gocui.MouseWheelDown,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.scrollDownMain,
|
||||
Description: gui.Tr.SLocalize("ScrollDown"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.MouseWheelUp,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.scrollUpMain,
|
||||
Description: gui.Tr.SLocalize("ScrollUp"),
|
||||
},
|
||||
func (gui *Gui) GetContextMap() map[string][]*Binding {
|
||||
return map[string][]*Binding{
|
||||
"normal": {
|
||||
{
|
||||
ViewName: "secondary",
|
||||
Key: gocui.MouseLeft,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleMouseDownSecondary,
|
||||
},
|
||||
"staging": {
|
||||
{
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyEsc,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStagingEscape,
|
||||
Description: gui.Tr.SLocalize("EscapeStaging"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyArrowUp,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStagingPrevLine,
|
||||
Description: gui.Tr.SLocalize("PrevLine"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyArrowDown,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStagingNextLine,
|
||||
Description: gui.Tr.SLocalize("NextLine"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'k',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStagingPrevLine,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'j',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStagingNextLine,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.MouseWheelUp,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStagingPrevLine,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.MouseWheelDown,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStagingNextLine,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyArrowLeft,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStagingPrevHunk,
|
||||
Description: gui.Tr.SLocalize("PrevHunk"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyArrowRight,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStagingNextHunk,
|
||||
Description: gui.Tr.SLocalize("NextHunk"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'h',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStagingPrevHunk,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'l',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStagingNextHunk,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeySpace,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStageLine,
|
||||
Description: gui.Tr.SLocalize("StageLine"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'a',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStageHunk,
|
||||
Description: gui.Tr.SLocalize("StageHunk"),
|
||||
},
|
||||
{
|
||||
ViewName: "main",
|
||||
Key: gocui.MouseWheelDown,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.scrollDownMain,
|
||||
Description: gui.Tr.SLocalize("ScrollDown"),
|
||||
Alternative: "fn+up",
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.MouseWheelUp,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.scrollUpMain,
|
||||
Description: gui.Tr.SLocalize("ScrollUp"),
|
||||
Alternative: "fn+down",
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.MouseLeft,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleMouseDownMain,
|
||||
},
|
||||
"merging": {
|
||||
{
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyEsc,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleEscapeMerge,
|
||||
Description: gui.Tr.SLocalize("EscapeStaging"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeySpace,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handlePickHunk,
|
||||
Description: gui.Tr.SLocalize("PickHunk"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'b',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handlePickBothHunks,
|
||||
Description: gui.Tr.SLocalize("PickBothHunks"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyArrowLeft,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectPrevConflict,
|
||||
Description: gui.Tr.SLocalize("PrevConflict"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyArrowRight,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectNextConflict,
|
||||
},
|
||||
"staging": {
|
||||
{
|
||||
ViewName: "secondary",
|
||||
Key: gocui.MouseLeft,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleTogglePanelClick,
|
||||
},
|
||||
{
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyEsc,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStagingEscape,
|
||||
Description: gui.Tr.SLocalize("ReturnToFilesPanel"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyArrowUp,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectPrevLine,
|
||||
Description: gui.Tr.SLocalize("PrevLine"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyArrowDown,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectNextLine,
|
||||
Description: gui.Tr.SLocalize("NextLine"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'k',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectPrevLine,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'j',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectNextLine,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyArrowLeft,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectPrevHunk,
|
||||
Description: gui.Tr.SLocalize("PrevHunk"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyArrowRight,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectNextHunk,
|
||||
Description: gui.Tr.SLocalize("NextHunk"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'h',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectPrevHunk,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'l',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectNextHunk,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeySpace,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleStageSelection,
|
||||
Description: gui.Tr.SLocalize("StageSelection"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'd',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleResetSelection,
|
||||
Description: gui.Tr.SLocalize("ResetSelection"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'v',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleToggleSelectRange,
|
||||
Description: gui.Tr.SLocalize("ToggleDragSelect"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'a',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleToggleSelectHunk,
|
||||
Description: gui.Tr.SLocalize("ToggleSelectHunk"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyTab,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleTogglePanel,
|
||||
Description: gui.Tr.SLocalize("TogglePanel"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.MouseLeft,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleMouseDown,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.MouseLeft,
|
||||
Modifier: gocui.ModMotion,
|
||||
Handler: gui.handleMouseDrag,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.MouseWheelUp,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleMouseScrollUp,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.MouseWheelDown,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleMouseScrollDown,
|
||||
},
|
||||
},
|
||||
"patch-building": {
|
||||
{
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyEsc,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleEscapePatchBuildingPanel,
|
||||
Description: gui.Tr.SLocalize("ExitLineByLineMode"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyArrowUp,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectPrevLine,
|
||||
Description: gui.Tr.SLocalize("PrevLine"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyArrowDown,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectNextLine,
|
||||
Description: gui.Tr.SLocalize("NextLine"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'k',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectPrevLine,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'j',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectNextLine,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.MouseWheelUp,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectPrevLine,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.MouseWheelDown,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectNextLine,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyArrowLeft,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectPrevHunk,
|
||||
Description: gui.Tr.SLocalize("PrevHunk"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyArrowRight,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectNextHunk,
|
||||
Description: gui.Tr.SLocalize("NextHunk"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'h',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectPrevHunk,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'l',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectNextHunk,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeySpace,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleAddSelectionToPatch,
|
||||
Description: gui.Tr.SLocalize("StageSelection"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'd',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleRemoveSelectionFromPatch,
|
||||
Description: gui.Tr.SLocalize("ResetSelection"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'v',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleToggleSelectRange,
|
||||
Description: gui.Tr.SLocalize("ToggleDragSelect"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'a',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleToggleSelectHunk,
|
||||
Description: gui.Tr.SLocalize("ToggleSelectHunk"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.MouseLeft,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleMouseDown,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.MouseLeft,
|
||||
Modifier: gocui.ModMotion,
|
||||
Handler: gui.handleMouseDrag,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.MouseWheelUp,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleMouseScrollUp,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.MouseWheelDown,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleMouseScrollDown,
|
||||
},
|
||||
},
|
||||
"merging": {
|
||||
{
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyEsc,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleEscapeMerge,
|
||||
Description: gui.Tr.SLocalize("ReturnToFilesPanel"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeySpace,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handlePickHunk,
|
||||
Description: gui.Tr.SLocalize("PickHunk"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'b',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handlePickBothHunks,
|
||||
Description: gui.Tr.SLocalize("PickBothHunks"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyArrowLeft,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectPrevConflict,
|
||||
Description: gui.Tr.SLocalize("PrevConflict"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyArrowRight,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectNextConflict,
|
||||
|
||||
Description: gui.Tr.SLocalize("NextConflict"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyArrowUp,
|
||||
Description: gui.Tr.SLocalize("NextConflict"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyArrowUp,
|
||||
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectTop,
|
||||
Description: gui.Tr.SLocalize("SelectTop"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyArrowDown,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectBottom,
|
||||
Description: gui.Tr.SLocalize("SelectBottom"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.MouseWheelUp,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectTop,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.MouseWheelDown,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectBottom,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'h',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectPrevConflict,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'l',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectNextConflict,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'k',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectTop,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'j',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectBottom,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'z',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handlePopFileSnapshot,
|
||||
Description: gui.Tr.SLocalize("Undo"),
|
||||
},
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectTop,
|
||||
Description: gui.Tr.SLocalize("SelectTop"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.KeyArrowDown,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectBottom,
|
||||
Description: gui.Tr.SLocalize("SelectBottom"),
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.MouseWheelUp,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectTop,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: gocui.MouseWheelDown,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectBottom,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'h',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectPrevConflict,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'l',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectNextConflict,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'k',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectTop,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'j',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleSelectBottom,
|
||||
}, {
|
||||
ViewName: "main",
|
||||
Key: 'z',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handlePopFileSnapshot,
|
||||
Description: gui.Tr.SLocalize("Undo"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
)
|
||||
|
||||
// Currently there are two 'pseudo-panels' that make use of this 'pseudo-panel'.
|
||||
// One is the staging panel where we stage files line-by-line, the other is the
|
||||
// patch building panel where we add lines of an old commit's file to a patch.
|
||||
// This file contains the logic around selecting lines and displaying the diffs
|
||||
// staging_panel.go and patch_building_panel.go have functions specific to their
|
||||
// use cases
|
||||
|
||||
// these represent what select mode we're in
|
||||
const (
|
||||
LINE = iota
|
||||
RANGE
|
||||
HUNK
|
||||
)
|
||||
|
||||
// returns whether the patch is empty so caller can escape if necessary
|
||||
// both diffs should be non-coloured because we'll parse them and colour them here
|
||||
func (gui *Gui) refreshLineByLinePanel(diff string, secondaryDiff string, secondaryFocused bool, selectedLineIdx int) (bool, error) {
|
||||
state := gui.State.Panels.LineByLine
|
||||
|
||||
patchParser, err := commands.NewPatchParser(gui.Log, diff)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if len(patchParser.StageableLines) == 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var firstLineIdx int
|
||||
var lastLineIdx int
|
||||
selectMode := LINE
|
||||
// if we have clicked from the outside to focus the main view we'll pass in a non-negative line index so that we can instantly select that line
|
||||
if selectedLineIdx >= 0 {
|
||||
selectMode = RANGE
|
||||
firstLineIdx, lastLineIdx = selectedLineIdx, selectedLineIdx
|
||||
} else if state != nil {
|
||||
if state.SelectMode == HUNK {
|
||||
// this is tricky: we need to find out which hunk we just staged based on our old `state.PatchParser` (as opposed to the new `patchParser`)
|
||||
// we do this by getting the first line index of the original hunk, then
|
||||
// finding the next stageable line, then getting its containing hunk
|
||||
// in the new diff
|
||||
selectMode = HUNK
|
||||
prevNewHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0)
|
||||
selectedLineIdx = patchParser.GetNextStageableLineIndex(prevNewHunk.FirstLineIdx)
|
||||
newHunk := patchParser.GetHunkContainingLine(selectedLineIdx, 0)
|
||||
firstLineIdx, lastLineIdx = newHunk.FirstLineIdx, newHunk.LastLineIdx
|
||||
} else {
|
||||
selectedLineIdx = patchParser.GetNextStageableLineIndex(state.SelectedLineIdx)
|
||||
firstLineIdx, lastLineIdx = selectedLineIdx, selectedLineIdx
|
||||
}
|
||||
} else {
|
||||
selectedLineIdx = patchParser.StageableLines[0]
|
||||
firstLineIdx, lastLineIdx = selectedLineIdx, selectedLineIdx
|
||||
}
|
||||
|
||||
gui.State.Panels.LineByLine = &lineByLinePanelState{
|
||||
PatchParser: patchParser,
|
||||
SelectedLineIdx: selectedLineIdx,
|
||||
SelectMode: selectMode,
|
||||
FirstLineIdx: firstLineIdx,
|
||||
LastLineIdx: lastLineIdx,
|
||||
Diff: diff,
|
||||
SecondaryFocused: secondaryFocused,
|
||||
}
|
||||
|
||||
if err := gui.refreshMainView(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := gui.focusSelection(selectMode == HUNK); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
secondaryView := gui.getSecondaryView()
|
||||
secondaryView.Highlight = true
|
||||
secondaryView.Wrap = false
|
||||
|
||||
secondaryPatchParser, err := commands.NewPatchParser(gui.Log, secondaryDiff)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
return gui.setViewContent(gui.g, gui.getSecondaryView(), secondaryPatchParser.Render(-1, -1, nil))
|
||||
})
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSelectPrevLine(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.handleCycleLine(-1)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSelectNextLine(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.handleCycleLine(+1)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSelectPrevHunk(g *gocui.Gui, v *gocui.View) error {
|
||||
state := gui.State.Panels.LineByLine
|
||||
newHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, -1)
|
||||
|
||||
return gui.selectNewHunk(newHunk)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSelectNextHunk(g *gocui.Gui, v *gocui.View) error {
|
||||
state := gui.State.Panels.LineByLine
|
||||
newHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 1)
|
||||
|
||||
return gui.selectNewHunk(newHunk)
|
||||
}
|
||||
|
||||
func (gui *Gui) selectNewHunk(newHunk *commands.PatchHunk) error {
|
||||
state := gui.State.Panels.LineByLine
|
||||
state.SelectedLineIdx = state.PatchParser.GetNextStageableLineIndex(newHunk.FirstLineIdx)
|
||||
if state.SelectMode == HUNK {
|
||||
state.FirstLineIdx, state.LastLineIdx = newHunk.FirstLineIdx, newHunk.LastLineIdx
|
||||
} else {
|
||||
state.FirstLineIdx, state.LastLineIdx = state.SelectedLineIdx, state.SelectedLineIdx
|
||||
}
|
||||
|
||||
if err := gui.refreshMainView(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.focusSelection(true)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCycleLine(change int) error {
|
||||
state := gui.State.Panels.LineByLine
|
||||
|
||||
if state.SelectMode == HUNK {
|
||||
newHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, change)
|
||||
return gui.selectNewHunk(newHunk)
|
||||
}
|
||||
|
||||
return gui.handleSelectNewLine(state.SelectedLineIdx + change)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSelectNewLine(newSelectedLineIdx int) error {
|
||||
state := gui.State.Panels.LineByLine
|
||||
|
||||
if newSelectedLineIdx < 0 {
|
||||
newSelectedLineIdx = 0
|
||||
} else if newSelectedLineIdx > len(state.PatchParser.PatchLines)-1 {
|
||||
newSelectedLineIdx = len(state.PatchParser.PatchLines) - 1
|
||||
}
|
||||
|
||||
state.SelectedLineIdx = newSelectedLineIdx
|
||||
|
||||
if state.SelectMode == RANGE {
|
||||
if state.SelectedLineIdx < state.FirstLineIdx {
|
||||
state.FirstLineIdx = state.SelectedLineIdx
|
||||
} else {
|
||||
state.LastLineIdx = state.SelectedLineIdx
|
||||
}
|
||||
} else {
|
||||
state.LastLineIdx = state.SelectedLineIdx
|
||||
state.FirstLineIdx = state.SelectedLineIdx
|
||||
}
|
||||
|
||||
if err := gui.refreshMainView(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.focusSelection(false)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMouseDown(g *gocui.Gui, v *gocui.View) error {
|
||||
state := gui.State.Panels.LineByLine
|
||||
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
newSelectedLineIdx := v.SelectedLineIdx()
|
||||
state.FirstLineIdx = newSelectedLineIdx
|
||||
state.LastLineIdx = newSelectedLineIdx
|
||||
|
||||
state.SelectMode = RANGE
|
||||
|
||||
return gui.handleSelectNewLine(newSelectedLineIdx)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMouseDrag(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.handleSelectNewLine(v.SelectedLineIdx())
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMouseScrollUp(g *gocui.Gui, v *gocui.View) error {
|
||||
state := gui.State.Panels.LineByLine
|
||||
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
state.SelectMode = LINE
|
||||
|
||||
return gui.handleCycleLine(-1)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMouseScrollDown(g *gocui.Gui, v *gocui.View) error {
|
||||
state := gui.State.Panels.LineByLine
|
||||
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
state.SelectMode = LINE
|
||||
|
||||
return gui.handleCycleLine(1)
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshMainView() error {
|
||||
state := gui.State.Panels.LineByLine
|
||||
|
||||
var includedLineIndices []int
|
||||
// I'd prefer not to have knowledge of contexts using this file but I'm not sure
|
||||
// how to get around this
|
||||
if gui.State.Context == "patch-building" {
|
||||
filename := gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLine].Name
|
||||
includedLineIndices = gui.GitCommand.PatchManager.GetFileIncLineIndices(filename)
|
||||
}
|
||||
colorDiff := state.PatchParser.Render(state.FirstLineIdx, state.LastLineIdx, includedLineIndices)
|
||||
|
||||
mainView := gui.getMainView()
|
||||
mainView.Highlight = true
|
||||
mainView.Wrap = false
|
||||
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
return gui.setViewContent(gui.g, gui.getMainView(), colorDiff)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// focusSelection works out the best focus for the staging panel given the
|
||||
// selected line and size of the hunk
|
||||
func (gui *Gui) focusSelection(includeCurrentHunk bool) error {
|
||||
stagingView := gui.getMainView()
|
||||
state := gui.State.Panels.LineByLine
|
||||
|
||||
_, viewHeight := stagingView.Size()
|
||||
bufferHeight := viewHeight - 1
|
||||
_, origin := stagingView.Origin()
|
||||
|
||||
firstLineIdx := state.SelectedLineIdx
|
||||
lastLineIdx := state.SelectedLineIdx
|
||||
|
||||
if includeCurrentHunk {
|
||||
hunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0)
|
||||
firstLineIdx = hunk.FirstLineIdx
|
||||
lastLineIdx = hunk.LastLineIdx
|
||||
}
|
||||
|
||||
margin := 0 // we may want to have a margin in place to show context but right now I'm thinking we keep this at zero
|
||||
|
||||
var newOrigin int
|
||||
if firstLineIdx-origin < margin {
|
||||
newOrigin = firstLineIdx - margin
|
||||
} else if lastLineIdx-origin > bufferHeight-margin {
|
||||
newOrigin = lastLineIdx - bufferHeight + margin
|
||||
} else {
|
||||
newOrigin = origin
|
||||
}
|
||||
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
if err := stagingView.SetOrigin(0, newOrigin); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return stagingView.SetCursor(0, state.SelectedLineIdx-newOrigin)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleToggleSelectRange(g *gocui.Gui, v *gocui.View) error {
|
||||
state := gui.State.Panels.LineByLine
|
||||
if state.SelectMode == RANGE {
|
||||
state.SelectMode = LINE
|
||||
} else {
|
||||
state.SelectMode = RANGE
|
||||
}
|
||||
state.FirstLineIdx, state.LastLineIdx = state.SelectedLineIdx, state.SelectedLineIdx
|
||||
|
||||
return gui.refreshMainView()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleToggleSelectHunk(g *gocui.Gui, v *gocui.View) error {
|
||||
state := gui.State.Panels.LineByLine
|
||||
|
||||
if state.SelectMode == HUNK {
|
||||
state.SelectMode = LINE
|
||||
state.FirstLineIdx, state.LastLineIdx = state.SelectedLineIdx, state.SelectedLineIdx
|
||||
} else {
|
||||
state.SelectMode = HUNK
|
||||
selectedHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0)
|
||||
state.FirstLineIdx, state.LastLineIdx = selectedHunk.FirstLineIdx, selectedHunk.LastLineIdx
|
||||
}
|
||||
|
||||
if err := gui.refreshMainView(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.focusSelection(state.SelectMode == HUNK)
|
||||
}
|
||||
+22
-4
@@ -4,13 +4,14 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// list panel functions
|
||||
|
||||
func (gui *Gui) handleMenuSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.focusPoint(0, gui.State.Panels.Menu.SelectedLine, v)
|
||||
return gui.focusPoint(0, gui.State.Panels.Menu.SelectedLine, gui.State.MenuItemCount, v)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMenuNextLine(g *gocui.Gui, v *gocui.View) error {
|
||||
@@ -51,8 +52,9 @@ func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.returnFocus(g, v)
|
||||
}
|
||||
|
||||
func (gui *Gui) createMenu(title string, items interface{}, handlePress func(int) error) error {
|
||||
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
|
||||
@@ -61,7 +63,7 @@ func (gui *Gui) createMenu(title string, items interface{}, handlePress func(int
|
||||
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 = gocui.ColorWhite
|
||||
menuView.FgColor = theme.GocuiDefaultTextColor
|
||||
menuView.Clear()
|
||||
fmt.Fprint(menuView, list)
|
||||
gui.State.Panels.Menu.SelectedLine = 0
|
||||
@@ -80,7 +82,11 @@ func (gui *Gui) createMenu(title string, items interface{}, handlePress func(int
|
||||
return gui.returnFocus(gui.g, menuView)
|
||||
}
|
||||
|
||||
for _, key := range []gocui.Key{gocui.KeySpace, gocui.KeyEnter} {
|
||||
gui.State.Panels.Menu.OnPress = wrappedHandlePress
|
||||
|
||||
for _, key := range []gocui.Key{gocui.KeySpace, gocui.KeyEnter, 'y'} {
|
||||
_ = gui.g.DeleteKeybinding("menu", key, gocui.ModNone)
|
||||
|
||||
if err := gui.g.SetKeybinding("menu", key, gocui.ModNone, wrappedHandlePress); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -97,3 +103,15 @@ func (gui *Gui) createMenu(title string, items interface{}, handlePress func(int
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMenuClick(g *gocui.Gui, v *gocui.View) error {
|
||||
itemCount := gui.State.MenuItemCount
|
||||
handleSelect := gui.handleMenuSelect
|
||||
selectedLine := &gui.State.Panels.Menu.SelectedLine
|
||||
|
||||
if err := gui.handleClick(v, itemCount, selectedLine, handleSelect); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.State.Panels.Menu.OnPress(g, v)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/golang-collections/collections/stack"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -23,7 +24,7 @@ func (gui *Gui) findConflicts(content string) ([]commands.Conflict, error) {
|
||||
for i, line := range utils.SplitLines(content) {
|
||||
trimmedLine := strings.TrimPrefix(line, "++")
|
||||
gui.Log.Info(trimmedLine)
|
||||
if trimmedLine == "<<<<<<< HEAD" || trimmedLine == "<<<<<<< MERGE_HEAD" || trimmedLine == "<<<<<<< Updated upstream" {
|
||||
if trimmedLine == "<<<<<<< HEAD" || trimmedLine == "<<<<<<< MERGE_HEAD" || trimmedLine == "<<<<<<< Updated upstream" || trimmedLine == "<<<<<<< ours" {
|
||||
newConflict = commands.Conflict{Start: i}
|
||||
} else if trimmedLine == "=======" {
|
||||
newConflict.Middle = i
|
||||
@@ -50,7 +51,7 @@ func (gui *Gui) coloredConflictFile(content string, conflicts []commands.Conflic
|
||||
conflict, remainingConflicts := gui.shiftConflict(conflicts)
|
||||
var outputBuffer bytes.Buffer
|
||||
for i, line := range utils.SplitLines(content) {
|
||||
colourAttr := color.FgWhite
|
||||
colourAttr := theme.DefaultTextColor
|
||||
if i == conflict.Start || i == conflict.Middle || i == conflict.End {
|
||||
colourAttr = color.FgRed
|
||||
}
|
||||
@@ -284,7 +285,7 @@ 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 {
|
||||
return gui.createConfirmationPanel(gui.g, gui.getFilesView(), "continue", gui.Tr.SLocalize("ConflictsResolved"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.createConfirmationPanel(gui.g, gui.getFilesView(), true, "continue", gui.Tr.SLocalize("ConflictsResolved"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.genericMergeCommand("continue")
|
||||
}, nil)
|
||||
}
|
||||
|
||||
@@ -49,5 +49,5 @@ func (gui *Gui) handleCreateOptionsMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
return bindings[index].Handler(g, v)
|
||||
}
|
||||
|
||||
return gui.createMenu(strings.Title(gui.Tr.SLocalize("menu")), bindings, handleMenuPress)
|
||||
return gui.createMenu(strings.Title(gui.Tr.SLocalize("menu")), bindings, len(bindings), handleMenuPress)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int) error {
|
||||
if !gui.GitCommand.PatchManager.CommitSelected() {
|
||||
return gui.handleEscapePatchBuildingPanel(gui.g, nil)
|
||||
}
|
||||
|
||||
gui.State.SplitMainPanel = true
|
||||
|
||||
gui.getMainView().Title = "Patch"
|
||||
gui.getSecondaryView().Title = "Custom Patch"
|
||||
|
||||
// get diff from commit file that's currently selected
|
||||
commitFile := gui.getSelectedCommitFile(gui.g)
|
||||
if commitFile == nil {
|
||||
return gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
|
||||
}
|
||||
|
||||
diff, err := gui.GitCommand.ShowCommitFile(commitFile.Sha, commitFile.Name, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
secondaryDiff := gui.GitCommand.PatchManager.RenderPatchForFile(commitFile.Name, true, false, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
empty, err := gui.refreshLineByLinePanel(diff, secondaryDiff, false, selectedLineIdx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if empty {
|
||||
return gui.handleEscapePatchBuildingPanel(gui.g, nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleAddSelectionToPatch(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"))
|
||||
}
|
||||
|
||||
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)
|
||||
if commitFile == nil {
|
||||
return gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
|
||||
}
|
||||
|
||||
gui.GitCommand.PatchManager.RemoveFileLineRange(commitFile.Name, state.FirstLineIdx, state.LastLineIdx)
|
||||
|
||||
if err := gui.refreshCommitFilesView(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.refreshPatchBuildingPanel(-1); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleEscapePatchBuildingPanel(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.State.Panels.LineByLine = nil
|
||||
gui.changeContext("normal")
|
||||
|
||||
if gui.GitCommand.PatchManager.IsEmpty() {
|
||||
gui.GitCommand.PatchManager.Reset()
|
||||
gui.State.SplitMainPanel = false
|
||||
}
|
||||
|
||||
return gui.switchFocus(gui.g, nil, gui.getCommitFilesView())
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshSecondaryPatchPanel() error {
|
||||
if gui.GitCommand.PatchManager.CommitSelected() {
|
||||
gui.State.SplitMainPanel = true
|
||||
secondaryView := gui.getSecondaryView()
|
||||
secondaryView.Highlight = true
|
||||
secondaryView.Wrap = false
|
||||
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
return gui.setViewContent(gui.g, gui.getSecondaryView(), gui.GitCommand.PatchManager.RenderAggregatedPatchColored(false))
|
||||
})
|
||||
} else {
|
||||
gui.State.SplitMainPanel = false
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"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"))
|
||||
}
|
||||
|
||||
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},
|
||||
}
|
||||
|
||||
selectedCommit := gui.getSelectedCommit(gui.g)
|
||||
if selectedCommit != nil && gui.GitCommand.PatchManager.CommitSha != selectedCommit.Sha {
|
||||
// adding this option to index 1
|
||||
options = append(
|
||||
options[:1],
|
||||
append(
|
||||
[]*patchMenuOption{
|
||||
{
|
||||
displayName: fmt.Sprintf("move patch to selected commit (%s)", selectedCommit.Sha),
|
||||
function: gui.handleMovePatchToSelectedCommit,
|
||||
},
|
||||
}, options[1:]...,
|
||||
)...,
|
||||
)
|
||||
}
|
||||
|
||||
handleMenuPress := func(index int) error {
|
||||
return options[index].function()
|
||||
}
|
||||
|
||||
return gui.createMenu(gui.Tr.SLocalize("PatchOptionsTitle"), options, len(options), handleMenuPress)
|
||||
}
|
||||
|
||||
func (gui *Gui) getPatchCommitIndex() int {
|
||||
for index, commit := range gui.State.Commits {
|
||||
if commit.Sha == gui.GitCommand.PatchManager.CommitSha {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (gui *Gui) validateNormalWorkingTreeState() (bool, error) {
|
||||
if gui.State.WorkingTreeState != "normal" {
|
||||
return false, gui.createErrorPanel(gui.g, gui.Tr.SLocalize("CantPatchWhileRebasingError"))
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (gui *Gui) returnFocusFromLineByLinePanelIfNecessary() error {
|
||||
if gui.State.Context == "patch-building" {
|
||||
return gui.handleEscapePatchBuildingPanel(gui.g, nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleDeletePatchFromCommit() error {
|
||||
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.returnFocusFromLineByLinePanelIfNecessary(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
|
||||
commitIndex := gui.getPatchCommitIndex()
|
||||
err := gui.GitCommand.DeletePatchesFromCommit(gui.State.Commits, commitIndex, gui.GitCommand.PatchManager)
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMovePatchToSelectedCommit() error {
|
||||
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.returnFocusFromLineByLinePanelIfNecessary(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
|
||||
commitIndex := gui.getPatchCommitIndex()
|
||||
err := gui.GitCommand.MovePatchToSelectedCommit(gui.State.Commits, commitIndex, gui.State.Panels.Commits.SelectedLine, gui.GitCommand.PatchManager)
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handlePullPatchIntoWorkingTree() error {
|
||||
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.returnFocusFromLineByLinePanelIfNecessary(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
|
||||
commitIndex := gui.getPatchCommitIndex()
|
||||
err := gui.GitCommand.PullPatchIntoIndex(gui.State.Commits, commitIndex, gui.GitCommand.PatchManager)
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleResetPatch() error {
|
||||
gui.GitCommand.PatchManager.Reset()
|
||||
return gui.refreshCommitFilesView()
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
// when a user runs lazygit with the LAZYGIT_NEW_DIR_FILE env variable defined
|
||||
// we will write the current directory to that file on exit so that their
|
||||
// shell can then change to that directory. That means you don't get kicked
|
||||
// back to the directory that you started with.
|
||||
func (gui *Gui) recordCurrentDirectory() error {
|
||||
if os.Getenv("LAZYGIT_NEW_DIR_FILE") == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// determine current directory, set it in LAZYGIT_NEW_DIR_FILE
|
||||
dirName, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.OSCommand.CreateFileWithContent(os.Getenv("LAZYGIT_NEW_DIR_FILE"), dirName)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleQuitWithoutChangingDirectory(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.State.RetainOriginalDir = true
|
||||
return gui.quit(v)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleQuit(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.State.RetainOriginalDir = false
|
||||
return gui.quit(v)
|
||||
}
|
||||
|
||||
func (gui *Gui) quit(v *gocui.View) error {
|
||||
if gui.State.Updating {
|
||||
return gui.createUpdateQuitConfirmation(gui.g, v)
|
||||
}
|
||||
if gui.Config.GetUserConfig().GetBool("confirmOnQuit") {
|
||||
return gui.createConfirmationPanel(gui.g, v, true, "", gui.Tr.SLocalize("ConfirmQuit"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gocui.ErrQuit
|
||||
}, nil)
|
||||
}
|
||||
|
||||
return gocui.ErrQuit
|
||||
}
|
||||
@@ -26,8 +26,13 @@ func (gui *Gui) handleCreateRebaseOptionsMenu(g *gocui.Gui, v *gocui.View) error
|
||||
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
|
||||
}
|
||||
return gui.genericMergeCommand(command)
|
||||
}
|
||||
|
||||
@@ -38,7 +43,7 @@ func (gui *Gui) handleCreateRebaseOptionsMenu(g *gocui.Gui, v *gocui.View) error
|
||||
title = gui.Tr.SLocalize("RebaseOptionsTitle")
|
||||
}
|
||||
|
||||
return gui.createMenu(title, options, handleMenuPress)
|
||||
return gui.createMenu(title, options, len(options), handleMenuPress)
|
||||
}
|
||||
|
||||
func (gui *Gui) genericMergeCommand(command string) error {
|
||||
@@ -77,8 +82,10 @@ func (gui *Gui) handleGenericMergeCommandResult(result error) error {
|
||||
return result
|
||||
} else if strings.Contains(result.Error(), "No changes - did you forget to use") {
|
||||
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(), "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(), gui.Tr.SLocalize("FoundConflictsTitle"), gui.Tr.SLocalize("FoundConflicts"),
|
||||
return gui.createConfirmationPanel(gui.g, gui.getFilesView(), true, gui.Tr.SLocalize("FoundConflictsTitle"), gui.Tr.SLocalize("FoundConflicts"),
|
||||
func(g *gocui.Gui, v *gocui.View) error {
|
||||
return nil
|
||||
}, func(g *gocui.Gui, v *gocui.View) error {
|
||||
|
||||
@@ -44,7 +44,7 @@ func (gui *Gui) handleCreateRecentReposMenu(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.Errors.ErrSwitchRepo
|
||||
}
|
||||
|
||||
return gui.createMenu(gui.Tr.SLocalize("RecentRepos"), recentRepos, handleMenuPress)
|
||||
return gui.createMenu(gui.Tr.SLocalize("RecentRepos"), recentRepos, len(recentRepos), handleMenuPress)
|
||||
}
|
||||
|
||||
// updateRecentRepoList registers the fact that we opened lazygit in this repo,
|
||||
|
||||
+92
-163
@@ -1,12 +1,17 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/git"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
)
|
||||
|
||||
func (gui *Gui) refreshStagingPanel() error {
|
||||
func (gui *Gui) refreshStagingPanel(forceSecondaryFocused bool, selectedLineIdx int) error {
|
||||
gui.State.SplitMainPanel = true
|
||||
|
||||
state := gui.State.Panels.LineByLine
|
||||
|
||||
file, err := gui.getSelectedFile(gui.g)
|
||||
if err != nil {
|
||||
if err != gui.Errors.ErrNoFiles {
|
||||
@@ -15,208 +20,132 @@ func (gui *Gui) refreshStagingPanel() error {
|
||||
return gui.handleStagingEscape(gui.g, nil)
|
||||
}
|
||||
|
||||
if !file.HasUnstagedChanges {
|
||||
if !file.HasUnstagedChanges && !file.HasStagedChanges {
|
||||
return gui.handleStagingEscape(gui.g, nil)
|
||||
}
|
||||
|
||||
secondaryFocused := false
|
||||
if forceSecondaryFocused {
|
||||
secondaryFocused = true
|
||||
} else {
|
||||
if state != nil {
|
||||
secondaryFocused = state.SecondaryFocused
|
||||
}
|
||||
}
|
||||
|
||||
if (secondaryFocused && !file.HasStagedChanges) || (!secondaryFocused && !file.HasUnstagedChanges) {
|
||||
secondaryFocused = !secondaryFocused
|
||||
}
|
||||
|
||||
if secondaryFocused {
|
||||
gui.getMainView().Title = gui.Tr.SLocalize("StagedChanges")
|
||||
gui.getSecondaryView().Title = gui.Tr.SLocalize("UnstagedChanges")
|
||||
} else {
|
||||
gui.getMainView().Title = gui.Tr.SLocalize("UnstagedChanges")
|
||||
gui.getSecondaryView().Title = gui.Tr.SLocalize("StagedChanges")
|
||||
}
|
||||
|
||||
// note for custom diffs, we'll need to send a flag here saying not to use the custom diff
|
||||
diff := gui.GitCommand.Diff(file, true)
|
||||
colorDiff := gui.GitCommand.Diff(file, false)
|
||||
diff := gui.GitCommand.Diff(file, true, secondaryFocused)
|
||||
secondaryDiff := gui.GitCommand.Diff(file, true, !secondaryFocused)
|
||||
|
||||
if len(diff) < 2 {
|
||||
return gui.handleStagingEscape(gui.g, nil)
|
||||
}
|
||||
|
||||
// parse the diff and store the line numbers of hunks and stageable lines
|
||||
// TODO: maybe instantiate this at application start
|
||||
p, err := git.NewPatchParser(gui.Log)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
hunkStarts, stageableLines, err := p.ParsePatch(diff)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var selectedLine int
|
||||
if gui.State.Panels.Staging != nil {
|
||||
end := len(stageableLines) - 1
|
||||
if end < gui.State.Panels.Staging.SelectedLine {
|
||||
selectedLine = end
|
||||
} else {
|
||||
selectedLine = gui.State.Panels.Staging.SelectedLine
|
||||
// if we have e.g. a deleted file with nothing else to the diff will have only
|
||||
// 4-5 lines in which case we'll swap panels
|
||||
if len(strings.Split(diff, "\n")) < 5 {
|
||||
if len(strings.Split(secondaryDiff, "\n")) < 5 {
|
||||
return gui.handleStagingEscape(gui.g, nil)
|
||||
}
|
||||
} else {
|
||||
selectedLine = 0
|
||||
secondaryFocused = !secondaryFocused
|
||||
diff, secondaryDiff = secondaryDiff, diff
|
||||
}
|
||||
|
||||
gui.State.Panels.Staging = &stagingPanelState{
|
||||
StageableLines: stageableLines,
|
||||
HunkStarts: hunkStarts,
|
||||
SelectedLine: selectedLine,
|
||||
Diff: diff,
|
||||
}
|
||||
|
||||
if len(stageableLines) == 0 {
|
||||
return gui.createErrorPanel(gui.g, "No lines to stage")
|
||||
}
|
||||
|
||||
if err := gui.focusLineAndHunk(); err != nil {
|
||||
empty, err := gui.refreshLineByLinePanel(diff, secondaryDiff, secondaryFocused, selectedLineIdx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mainView := gui.getMainView()
|
||||
mainView.Highlight = true
|
||||
mainView.Wrap = false
|
||||
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
return gui.setViewContent(gui.g, gui.getMainView(), colorDiff)
|
||||
})
|
||||
if empty {
|
||||
return gui.handleStagingEscape(gui.g, nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleTogglePanelClick(g *gocui.Gui, v *gocui.View) error {
|
||||
state := gui.State.Panels.LineByLine
|
||||
|
||||
state.SecondaryFocused = !state.SecondaryFocused
|
||||
|
||||
return gui.refreshStagingPanel(false, v.SelectedLineIdx())
|
||||
}
|
||||
|
||||
func (gui *Gui) handleTogglePanel(g *gocui.Gui, v *gocui.View) error {
|
||||
state := gui.State.Panels.LineByLine
|
||||
|
||||
state.SecondaryFocused = !state.SecondaryFocused
|
||||
return gui.refreshStagingPanel(false, -1)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStagingEscape(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.State.Panels.Staging = nil
|
||||
gui.State.Panels.LineByLine = nil
|
||||
|
||||
return gui.switchFocus(gui.g, nil, gui.getFilesView())
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStagingPrevLine(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.handleCycleLine(true)
|
||||
func (gui *Gui) handleStageSelection(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.applySelection(false)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStagingNextLine(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.handleCycleLine(false)
|
||||
func (gui *Gui) handleResetSelection(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.applySelection(true)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStagingPrevHunk(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.handleCycleHunk(true)
|
||||
}
|
||||
func (gui *Gui) applySelection(reverse bool) error {
|
||||
state := gui.State.Panels.LineByLine
|
||||
|
||||
func (gui *Gui) handleStagingNextHunk(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.handleCycleHunk(false)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCycleHunk(prev bool) error {
|
||||
state := gui.State.Panels.Staging
|
||||
lineNumbers := state.StageableLines
|
||||
currentLine := lineNumbers[state.SelectedLine]
|
||||
currentHunkIndex := utils.PrevIndex(state.HunkStarts, currentLine)
|
||||
var newHunkIndex int
|
||||
if prev {
|
||||
if currentHunkIndex == 0 {
|
||||
newHunkIndex = len(state.HunkStarts) - 1
|
||||
} else {
|
||||
newHunkIndex = currentHunkIndex - 1
|
||||
}
|
||||
} else {
|
||||
if currentHunkIndex == len(state.HunkStarts)-1 {
|
||||
newHunkIndex = 0
|
||||
} else {
|
||||
newHunkIndex = currentHunkIndex + 1
|
||||
}
|
||||
if !reverse && state.SecondaryFocused {
|
||||
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("CantStageStaged"))
|
||||
}
|
||||
|
||||
state.SelectedLine = utils.NextIndex(lineNumbers, state.HunkStarts[newHunkIndex])
|
||||
|
||||
return gui.focusLineAndHunk()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCycleLine(prev bool) error {
|
||||
state := gui.State.Panels.Staging
|
||||
lineNumbers := state.StageableLines
|
||||
currentLine := lineNumbers[state.SelectedLine]
|
||||
var newIndex int
|
||||
if prev {
|
||||
newIndex = utils.PrevIndex(lineNumbers, currentLine)
|
||||
} else {
|
||||
newIndex = utils.NextIndex(lineNumbers, currentLine)
|
||||
}
|
||||
state.SelectedLine = newIndex
|
||||
|
||||
return gui.focusLineAndHunk()
|
||||
}
|
||||
|
||||
// focusLineAndHunk works out the best focus for the staging panel given the
|
||||
// selected line and size of the hunk
|
||||
func (gui *Gui) focusLineAndHunk() error {
|
||||
stagingView := gui.getMainView()
|
||||
state := gui.State.Panels.Staging
|
||||
|
||||
lineNumber := state.StageableLines[state.SelectedLine]
|
||||
|
||||
// we want the bottom line of the view buffer to ideally be the bottom line
|
||||
// of the hunk, but if the hunk is too big we'll just go three lines beyond
|
||||
// the currently selected line so that the user can see the context
|
||||
var bottomLine int
|
||||
nextHunkStartIndex := utils.NextIndex(state.HunkStarts, lineNumber)
|
||||
if nextHunkStartIndex == 0 {
|
||||
// for now linesHeight is an efficient means of getting the number of lines
|
||||
// in the patch. However if we introduce word wrap we'll need to update this
|
||||
bottomLine = stagingView.LinesHeight() - 1
|
||||
} else {
|
||||
bottomLine = state.HunkStarts[nextHunkStartIndex] - 1
|
||||
}
|
||||
|
||||
hunkStartIndex := utils.PrevIndex(state.HunkStarts, lineNumber)
|
||||
hunkStart := state.HunkStarts[hunkStartIndex]
|
||||
// if it's the first hunk we'll also show the diff header
|
||||
if hunkStartIndex == 0 {
|
||||
hunkStart = 0
|
||||
}
|
||||
|
||||
_, height := stagingView.Size()
|
||||
// if this hunk is too big, we will just ensure that the user can at least
|
||||
// see three lines of context below the cursor
|
||||
if bottomLine-hunkStart > height {
|
||||
bottomLine = lineNumber + 3
|
||||
}
|
||||
|
||||
return gui.generalFocusLine(lineNumber, bottomLine, stagingView)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStageHunk(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.handleStageLineOrHunk(true)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStageLine(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.handleStageLineOrHunk(false)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStageLineOrHunk(hunk bool) error {
|
||||
state := gui.State.Panels.Staging
|
||||
p, err := git.NewPatchModifier(gui.Log)
|
||||
file, err := gui.getSelectedFile(gui.g)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentLine := state.StageableLines[state.SelectedLine]
|
||||
var patch string
|
||||
if hunk {
|
||||
patch, err = p.ModifyPatchForHunk(state.Diff, state.HunkStarts, currentLine)
|
||||
} else {
|
||||
patch, err = p.ModifyPatchForLine(state.Diff, currentLine)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
patch := commands.ModifiedPatchForRange(gui.Log, file.Name, state.Diff, state.FirstLineIdx, state.LastLineIdx, reverse, false)
|
||||
|
||||
// for logging purposes
|
||||
// ioutil.WriteFile("patch.diff", []byte(patch), 0600)
|
||||
if patch == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// apply the patch then refresh this panel
|
||||
// create a new temp file with the patch, then call git apply with that patch
|
||||
_, err = gui.GitCommand.ApplyPatch(patch)
|
||||
applyFlags := []string{}
|
||||
if !reverse || state.SecondaryFocused {
|
||||
applyFlags = append(applyFlags, "cached")
|
||||
}
|
||||
err = gui.GitCommand.ApplyPatch(patch, applyFlags...)
|
||||
if err != nil {
|
||||
return err
|
||||
return gui.createErrorPanel(gui.g, err.Error())
|
||||
}
|
||||
|
||||
if state.SelectMode == RANGE {
|
||||
state.SelectMode = LINE
|
||||
}
|
||||
|
||||
if err := gui.refreshFiles(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.refreshStagingPanel(); err != nil {
|
||||
if err := gui.refreshStagingPanel(false, -1); err != nil {
|
||||
return err
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
+11
-7
@@ -24,14 +24,19 @@ func (gui *Gui) handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.State.SplitMainPanel = false
|
||||
|
||||
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.getMainView().Title = "Stash"
|
||||
|
||||
stashEntry := gui.getSelectedStashEntry(v)
|
||||
if stashEntry == nil {
|
||||
return gui.renderString(g, "main", gui.Tr.SLocalize("NoStashEntries"))
|
||||
}
|
||||
if err := gui.focusPoint(0, gui.State.Panels.Stash.SelectedLine, v); err != nil {
|
||||
if err := gui.focusPoint(0, gui.State.Panels.Stash.SelectedLine, len(gui.State.StashEntries), v); err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
@@ -107,7 +112,7 @@ func (gui *Gui) handleStashPop(g *gocui.Gui, v *gocui.View) error {
|
||||
func (gui *Gui) handleStashDrop(g *gocui.Gui, v *gocui.View) error {
|
||||
title := gui.Tr.SLocalize("StashDrop")
|
||||
message := gui.Tr.SLocalize("SureDropStashEntry")
|
||||
return gui.createConfirmationPanel(g, v, title, message, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.createConfirmationPanel(g, v, true, title, message, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.stashDo(g, v, "drop")
|
||||
}, nil)
|
||||
}
|
||||
@@ -130,16 +135,15 @@ func (gui *Gui) stashDo(g *gocui.Gui, v *gocui.View, method string) error {
|
||||
return gui.refreshFiles()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStashSave(g *gocui.Gui, filesView *gocui.View) error {
|
||||
func (gui *Gui) handleStashSave(stashFunc func(message string) error) error {
|
||||
if len(gui.trackedFiles()) == 0 && len(gui.stagedFiles()) == 0 {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoTrackedStagedFilesStash"))
|
||||
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NoTrackedStagedFilesStash"))
|
||||
}
|
||||
gui.createPromptPanel(g, filesView, gui.Tr.SLocalize("StashChanges"), func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.StashSave(gui.trimmedContent(v)); err != nil {
|
||||
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())
|
||||
}
|
||||
gui.refreshStashEntries(g)
|
||||
return gui.refreshFiles()
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
+54
-12
@@ -10,6 +10,8 @@ import (
|
||||
)
|
||||
|
||||
func (gui *Gui) refreshStatus(g *gocui.Gui) error {
|
||||
state := gui.State.Panels.Status
|
||||
|
||||
v, err := g.View("status")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -19,53 +21,93 @@ func (gui *Gui) refreshStatus(g *gocui.Gui) error {
|
||||
// contents end up cleared
|
||||
g.Update(func(*gocui.Gui) error {
|
||||
v.Clear()
|
||||
pushables, pullables := gui.GitCommand.GetCurrentBranchUpstreamDifferenceCount()
|
||||
fmt.Fprint(v, "↑"+pushables+"↓"+pullables)
|
||||
branches := gui.State.Branches
|
||||
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
|
||||
|
||||
if gui.State.WorkingTreeState != "normal" {
|
||||
fmt.Fprint(v, utils.ColoredString(fmt.Sprintf(" (%s)", gui.State.WorkingTreeState), color.FgYellow))
|
||||
status += utils.ColoredString(fmt.Sprintf(" (%s)", gui.State.WorkingTreeState), color.FgYellow)
|
||||
}
|
||||
|
||||
if len(branches) == 0 {
|
||||
return nil
|
||||
if len(branches) > 0 {
|
||||
branch := branches[0]
|
||||
name := utils.ColoredString(branch.Name, branch.GetColor())
|
||||
repoName := utils.GetCurrentRepoName()
|
||||
status += fmt.Sprintf(" %s → %s", repoName, name)
|
||||
}
|
||||
branch := branches[0]
|
||||
name := utils.ColoredString(branch.Name, branch.GetColor())
|
||||
repo := utils.GetCurrentRepoName()
|
||||
fmt.Fprint(v, " "+repo+" → "+name)
|
||||
|
||||
fmt.Fprint(v, status)
|
||||
return nil
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runeCount(str string) int {
|
||||
return len([]rune(str))
|
||||
}
|
||||
|
||||
func cursorInSubstring(cx int, prefix string, substring string) bool {
|
||||
return cx >= runeCount(prefix) && cx < runeCount(prefix+substring)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCheckForUpdate(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.Updater.CheckForNewUpdate(gui.onUserUpdateCheckFinish, true)
|
||||
return gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("CheckingForUpdates"))
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStatusClick(g *gocui.Gui, v *gocui.View) error {
|
||||
state := gui.State.Panels.Status
|
||||
|
||||
cx, _ := v.Cursor()
|
||||
upstreamStatus := fmt.Sprintf("↑%s↓%s", state.pushables, state.pullables)
|
||||
repoName := utils.GetCurrentRepoName()
|
||||
gui.Log.Warn(gui.State.WorkingTreeState)
|
||||
switch gui.State.WorkingTreeState {
|
||||
case "rebasing", "merging":
|
||||
workingTreeStatus := fmt.Sprintf("(%s)", gui.State.WorkingTreeState)
|
||||
if cursorInSubstring(cx, upstreamStatus+" ", workingTreeStatus) {
|
||||
return gui.handleCreateRebaseOptionsMenu(gui.g, v)
|
||||
}
|
||||
if cursorInSubstring(cx, upstreamStatus+" "+workingTreeStatus+" ", repoName) {
|
||||
return gui.handleCreateRecentReposMenu(gui.g, v)
|
||||
}
|
||||
default:
|
||||
if cursorInSubstring(cx, upstreamStatus+" ", repoName) {
|
||||
return gui.handleCreateRecentReposMenu(gui.g, v)
|
||||
}
|
||||
}
|
||||
|
||||
return gui.handleStatusSelect(gui.g, v)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStatusSelect(g *gocui.Gui, v *gocui.View) error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.State.SplitMainPanel = false
|
||||
|
||||
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.getMainView().Title = ""
|
||||
|
||||
magenta := color.New(color.FgMagenta)
|
||||
|
||||
dashboardString := strings.Join(
|
||||
[]string{
|
||||
lazygitTitle(),
|
||||
"Copyright (c) 2018 Jesse Duffield",
|
||||
"Keybindings: https://github.com/jesseduffield/lazygit/blob/master/docs/Keybindings.md",
|
||||
"Keybindings: https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings",
|
||||
"Config Options: https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md",
|
||||
"Tutorial: https://youtu.be/VDXvbHZYeKY",
|
||||
"Raise an Issue: https://github.com/jesseduffield/lazygit/issues",
|
||||
magenta.Sprint("Buy Jesse a coffee: https://donorbox.org/lazygit"), // caffeine ain't free
|
||||
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)
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
// GetAttribute gets the gocui color attribute from the string
|
||||
func (gui *Gui) GetAttribute(key string) gocui.Attribute {
|
||||
colorMap := map[string]gocui.Attribute{
|
||||
"default": gocui.ColorDefault,
|
||||
"black": gocui.ColorBlack,
|
||||
"red": gocui.ColorRed,
|
||||
"green": gocui.ColorGreen,
|
||||
"yellow": gocui.ColorYellow,
|
||||
"blue": gocui.ColorBlue,
|
||||
"magenta": gocui.ColorMagenta,
|
||||
"cyan": gocui.ColorCyan,
|
||||
"white": gocui.ColorWhite,
|
||||
"bold": gocui.AttrBold,
|
||||
"reverse": gocui.AttrReverse,
|
||||
"underline": gocui.AttrUnderline,
|
||||
}
|
||||
value, present := colorMap[key]
|
||||
if present {
|
||||
return value
|
||||
}
|
||||
return gocui.ColorWhite
|
||||
}
|
||||
|
||||
// GetColor bitwise OR's a list of attributes obtained via the given keys
|
||||
func (gui *Gui) GetColor(keys []string) gocui.Attribute {
|
||||
var attribute gocui.Attribute
|
||||
for _, key := range keys {
|
||||
attribute = attribute | gui.GetAttribute(key)
|
||||
}
|
||||
return attribute
|
||||
}
|
||||
|
||||
// GetOptionsPanelTextColor gets the color of the options panel text
|
||||
func (gui *Gui) GetOptionsPanelTextColor() (gocui.Attribute, error) {
|
||||
userConfig := gui.Config.GetUserConfig()
|
||||
optionsColor := userConfig.GetStringSlice("gui.theme.optionsTextColor")
|
||||
return gui.GetColor(optionsColor), nil
|
||||
}
|
||||
|
||||
// SetColorScheme sets the color scheme for the app based on the user config
|
||||
func (gui *Gui) SetColorScheme() error {
|
||||
userConfig := gui.Config.GetUserConfig()
|
||||
activeBorderColor := userConfig.GetStringSlice("gui.theme.activeBorderColor")
|
||||
inactiveBorderColor := userConfig.GetStringSlice("gui.theme.inactiveBorderColor")
|
||||
gui.g.FgColor = gui.GetColor(inactiveBorderColor)
|
||||
gui.g.SelFgColor = gui.GetColor(activeBorderColor)
|
||||
return nil
|
||||
}
|
||||
+2
-2
@@ -6,7 +6,7 @@ func (gui *Gui) showUpdatePrompt(newVersion string) error {
|
||||
title := "New version available!"
|
||||
message := "Download latest version? (enter/esc)"
|
||||
currentView := gui.g.CurrentView()
|
||||
return gui.createConfirmationPanel(gui.g, currentView, title, message, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.createConfirmationPanel(gui.g, currentView, true, title, message, func(g *gocui.Gui, v *gocui.View) error {
|
||||
gui.startUpdating(newVersion)
|
||||
return nil
|
||||
}, nil)
|
||||
@@ -59,7 +59,7 @@ func (gui *Gui) onUpdateFinish(err error) error {
|
||||
func (gui *Gui) createUpdateQuitConfirmation(g *gocui.Gui, v *gocui.View) error {
|
||||
title := "Currently Updating"
|
||||
message := "An update is in progress. Are you sure you want to quit?"
|
||||
return gui.createConfirmationPanel(gui.g, v, title, message, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.createConfirmationPanel(gui.g, v, true, title, message, func(g *gocui.Gui, v *gocui.View) error {
|
||||
return gocui.ErrQuit
|
||||
}, nil)
|
||||
}
|
||||
|
||||
+106
-54
@@ -22,6 +22,7 @@ func (gui *Gui) refreshSidePanels(g *gocui.Gui) error {
|
||||
if err := gui.refreshCommits(g); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.refreshStashEntries(g)
|
||||
}
|
||||
|
||||
@@ -30,8 +31,13 @@ func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error {
|
||||
if v == nil || v.Name() == cyclableViews[len(cyclableViews)-1] {
|
||||
focusedViewName = cyclableViews[0]
|
||||
} else {
|
||||
// if we're in the commitFiles view we'll act like we're in the commits view
|
||||
viewName := v.Name()
|
||||
if viewName == "commitFiles" {
|
||||
viewName = "commits"
|
||||
}
|
||||
for i := range cyclableViews {
|
||||
if v.Name() == cyclableViews[i] {
|
||||
if viewName == cyclableViews[i] {
|
||||
focusedViewName = cyclableViews[i+1]
|
||||
break
|
||||
}
|
||||
@@ -39,7 +45,7 @@ func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error {
|
||||
message := gui.Tr.TemplateLocalize(
|
||||
"IssntListOfViews",
|
||||
Teml{
|
||||
"name": v.Name(),
|
||||
"name": viewName,
|
||||
},
|
||||
)
|
||||
gui.Log.Info(message)
|
||||
@@ -59,8 +65,13 @@ func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error {
|
||||
if v == nil || v.Name() == cyclableViews[0] {
|
||||
focusedViewName = cyclableViews[len(cyclableViews)-1]
|
||||
} else {
|
||||
// if we're in the commitFiles view we'll act like we're in the commits view
|
||||
viewName := v.Name()
|
||||
if viewName == "commitFiles" {
|
||||
viewName = "commits"
|
||||
}
|
||||
for i := range cyclableViews {
|
||||
if v.Name() == cyclableViews[i] {
|
||||
if viewName == cyclableViews[i] {
|
||||
focusedViewName = cyclableViews[i-1] // TODO: make this work properly
|
||||
break
|
||||
}
|
||||
@@ -68,7 +79,7 @@ func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error {
|
||||
message := gui.Tr.TemplateLocalize(
|
||||
"IssntListOfViews",
|
||||
Teml{
|
||||
"name": v.Name(),
|
||||
"name": viewName,
|
||||
},
|
||||
)
|
||||
gui.Log.Info(message)
|
||||
@@ -95,6 +106,8 @@ func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.handleBranchSelect(g, v)
|
||||
case "commits":
|
||||
return gui.handleCommitSelect(g, v)
|
||||
case "commitFiles":
|
||||
return gui.handleCommitFileSelect(g, v)
|
||||
case "stash":
|
||||
return gui.handleStashEntrySelect(g, v)
|
||||
case "confirmation":
|
||||
@@ -104,7 +117,7 @@ func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error {
|
||||
case "credentials":
|
||||
return gui.handleCredentialsViewFocused(g, v)
|
||||
case "main":
|
||||
if gui.State.Contexts["main"] == "merging" {
|
||||
if gui.State.Context == "merging" {
|
||||
return gui.refreshMergePanel()
|
||||
}
|
||||
v.Highlight = false
|
||||
@@ -126,18 +139,39 @@ func (gui *Gui) returnFocus(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.switchFocus(g, v, previousView)
|
||||
}
|
||||
|
||||
func (gui *Gui) goToSideView(sideViewName string) func(g *gocui.Gui, v *gocui.View) error {
|
||||
return func(g *gocui.Gui, v *gocui.View) error {
|
||||
view, err := g.View(sideViewName)
|
||||
if err != nil {
|
||||
gui.Log.Error(err)
|
||||
return nil
|
||||
}
|
||||
err = gui.closePopupPanels()
|
||||
if err != nil {
|
||||
gui.Log.Error(err)
|
||||
return nil
|
||||
}
|
||||
return gui.switchFocus(g, nil, view)
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) closePopupPanels() error {
|
||||
gui.onNewPopupPanel()
|
||||
err := gui.closeConfirmationPrompt(gui.g, true)
|
||||
if err != nil {
|
||||
gui.Log.Error(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// pass in oldView = nil if you don't want to be able to return to your old view
|
||||
// TODO: move some of this logic into our onFocusLost and onFocus hooks
|
||||
func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error {
|
||||
// we assume we'll never want to return focus to a confirmation panel i.e.
|
||||
// we should never stack confirmation panels
|
||||
if oldView != nil && oldView.Name() != "confirmation" {
|
||||
// second class panels should never have focus restored to them because
|
||||
// once they lose focus they are effectively 'destroyed'
|
||||
secondClassPanels := []string{"confirmation", "menu"}
|
||||
if !utils.IncludesString(secondClassPanels, oldView.Name()) {
|
||||
gui.State.PreviousView = oldView.Name()
|
||||
}
|
||||
// we assume we'll never want to return focus to a popup panel i.e.
|
||||
// we should never stack popup panels
|
||||
if oldView != nil && !gui.isPopupPanel(oldView.Name()) {
|
||||
gui.State.PreviousView = oldView.Name()
|
||||
}
|
||||
|
||||
gui.Log.Info("setting highlight to true for view" + newView.Name())
|
||||
@@ -165,49 +199,37 @@ func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) resetOrigin(v *gocui.View) error {
|
||||
if err := v.SetCursor(0, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = v.SetCursor(0, 0)
|
||||
return v.SetOrigin(0, 0)
|
||||
}
|
||||
|
||||
// if the cursor down past the last item, move it to the last line
|
||||
func (gui *Gui) focusPoint(cx int, cy int, v *gocui.View) error {
|
||||
if cy < 0 {
|
||||
func (gui *Gui) focusPoint(cx int, cy int, lineCount int, v *gocui.View) error {
|
||||
if cy < 0 || cy > lineCount {
|
||||
return nil
|
||||
}
|
||||
ox, oy := v.Origin()
|
||||
_, height := v.Size()
|
||||
|
||||
ly := height - 1
|
||||
if ly == -1 {
|
||||
ly = 0
|
||||
}
|
||||
|
||||
// if line is above origin, move origin and set cursor to zero
|
||||
// if line is below origin + height, move origin and set cursor to max
|
||||
// otherwise set cursor to value - origin
|
||||
if ly > v.LinesHeight() {
|
||||
if err := v.SetCursor(cx, cy); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := v.SetOrigin(ox, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if ly > lineCount {
|
||||
_ = v.SetCursor(cx, cy)
|
||||
_ = v.SetOrigin(ox, 0)
|
||||
} else if cy < oy {
|
||||
if err := v.SetCursor(cx, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := v.SetOrigin(ox, cy); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = v.SetCursor(cx, 0)
|
||||
_ = v.SetOrigin(ox, cy)
|
||||
} else if cy > oy+ly {
|
||||
if err := v.SetCursor(cx, ly); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := v.SetOrigin(ox, cy-ly); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = v.SetCursor(cx, ly)
|
||||
_ = v.SetOrigin(ox, cy-ly)
|
||||
} else {
|
||||
if err := v.SetCursor(cx, cy-oy); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = v.SetCursor(cx, cy-oy)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -278,11 +300,21 @@ func (gui *Gui) getMainView() *gocui.View {
|
||||
return v
|
||||
}
|
||||
|
||||
func (gui *Gui) getSecondaryView() *gocui.View {
|
||||
v, _ := gui.g.View("secondary")
|
||||
return v
|
||||
}
|
||||
|
||||
func (gui *Gui) getStashView() *gocui.View {
|
||||
v, _ := gui.g.View("stash")
|
||||
return v
|
||||
}
|
||||
|
||||
func (gui *Gui) getCommitFilesView() *gocui.View {
|
||||
v, _ := gui.g.View("commitFiles")
|
||||
return v
|
||||
}
|
||||
|
||||
func (gui *Gui) trimmedContent(v *gocui.View) string {
|
||||
return strings.TrimSpace(v.Buffer())
|
||||
}
|
||||
@@ -294,7 +326,7 @@ func (gui *Gui) currentViewName() string {
|
||||
|
||||
func (gui *Gui) resizeCurrentPopupPanel(g *gocui.Gui) error {
|
||||
v := g.CurrentView()
|
||||
if v.Name() == "commitMessage" || v.Name() == "credentials" || v.Name() == "confirmation" {
|
||||
if gui.isPopupPanel(v.Name()) {
|
||||
return gui.resizePopupPanel(g, v)
|
||||
}
|
||||
return nil
|
||||
@@ -336,13 +368,13 @@ func (gui *Gui) changeSelectedLine(line *int, total int, up bool) {
|
||||
return
|
||||
}
|
||||
|
||||
*line -= 1
|
||||
*line--
|
||||
} else {
|
||||
if *line == -1 || *line == total-1 {
|
||||
return
|
||||
}
|
||||
|
||||
*line += 1
|
||||
*line++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,7 +406,7 @@ func (gui *Gui) renderPanelOptions() error {
|
||||
case "menu":
|
||||
return gui.renderMenuOptions()
|
||||
case "main":
|
||||
if gui.State.Contexts["main"] == "merging" {
|
||||
if gui.State.Context == "merging" {
|
||||
return gui.renderMergeOptions()
|
||||
}
|
||||
}
|
||||
@@ -386,14 +418,34 @@ func (gui *Gui) handleFocusView(g *gocui.Gui, v *gocui.View) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (gui *Gui) isPopupPanel(viewName string) bool {
|
||||
return viewName == "commitMessage" || viewName == "credentials" || viewName == "confirmation" || viewName == "menu"
|
||||
}
|
||||
|
||||
func (gui *Gui) popupPanelFocused() bool {
|
||||
viewNames := []string{"commitMessage",
|
||||
"credentials",
|
||||
"menu"}
|
||||
for _, viewName := range viewNames {
|
||||
if gui.currentViewName() == viewName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return gui.isPopupPanel(gui.currentViewName())
|
||||
}
|
||||
|
||||
func (gui *Gui) handleClick(v *gocui.View, itemCount int, selectedLine *int, handleSelect func(*gocui.Gui, *gocui.View) error) error {
|
||||
if gui.popupPanelFocused() && v != nil && !gui.isPopupPanel(v.Name()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newSelectedLine := v.SelectedLineIdx()
|
||||
|
||||
if newSelectedLine < 0 {
|
||||
newSelectedLine = 0
|
||||
}
|
||||
|
||||
if newSelectedLine > itemCount-1 {
|
||||
newSelectedLine = itemCount - 1
|
||||
}
|
||||
|
||||
*selectedLine = newSelectedLine
|
||||
|
||||
return handleSelect(gui.g, v)
|
||||
}
|
||||
|
||||
+196
-79
@@ -28,12 +28,21 @@ func addDutch(i18nObject *i18n.Bundle) error {
|
||||
}, &i18n.Message{
|
||||
ID: "CommitsTitle",
|
||||
Other: "Commits",
|
||||
}, &i18n.Message{
|
||||
ID: "CommitsDiffTitle",
|
||||
Other: "Commits (specific diff mode)",
|
||||
}, &i18n.Message{
|
||||
ID: "CommitsDiff",
|
||||
Other: "select commit to diff with another commit",
|
||||
}, &i18n.Message{
|
||||
ID: "StashTitle",
|
||||
Other: "Stash",
|
||||
}, &i18n.Message{
|
||||
ID: "StagingMainTitle",
|
||||
Other: `Stage Lines/Hunks`,
|
||||
ID: "UnstagedChanges",
|
||||
Other: `Unstaged Changes`,
|
||||
}, &i18n.Message{
|
||||
ID: "StagedChanges",
|
||||
Other: `Staged Changes`,
|
||||
}, &i18n.Message{
|
||||
ID: "MergingMainTitle",
|
||||
Other: "Resolve merge conflicts",
|
||||
@@ -79,9 +88,6 @@ func addDutch(i18nObject *i18n.Bundle) error {
|
||||
}, &i18n.Message{
|
||||
ID: "execute",
|
||||
Other: "uitvoeren",
|
||||
}, &i18n.Message{
|
||||
ID: "stashFiles",
|
||||
Other: "stash-bestanden",
|
||||
}, &i18n.Message{
|
||||
ID: "open",
|
||||
Other: "open",
|
||||
@@ -106,9 +112,6 @@ func addDutch(i18nObject *i18n.Bundle) error {
|
||||
}, &i18n.Message{
|
||||
ID: "pull",
|
||||
Other: "pull",
|
||||
}, &i18n.Message{
|
||||
ID: "addPatch",
|
||||
Other: "bewerkingen toevoegen",
|
||||
}, &i18n.Message{
|
||||
ID: "edit",
|
||||
Other: "bewerken",
|
||||
@@ -157,9 +160,6 @@ func addDutch(i18nObject *i18n.Bundle) error {
|
||||
}, &i18n.Message{
|
||||
ID: "FileNoMergeCons",
|
||||
Other: "Dit bestand heeft geen merge conflicten",
|
||||
}, &i18n.Message{
|
||||
ID: "SureResetHardHead",
|
||||
Other: "Weet je het zeker dat je `reset --hard HEAD` en `clean -fd` wil uitvoeren? Het kan dat je hierdoor bestanden verliest",
|
||||
}, &i18n.Message{
|
||||
ID: "SureTo",
|
||||
Other: "Weet je het zeker dat je {{.fileName}} wilt {{.deleteVerb}} (je veranderingen zullen worden verwijderd)",
|
||||
@@ -340,9 +340,6 @@ func addDutch(i18nObject *i18n.Bundle) error {
|
||||
}, &i18n.Message{
|
||||
ID: "CantCloseConfirmationPrompt",
|
||||
Other: "Kon de bevestiging prompt niet sluiten: {{.error}}",
|
||||
}, &i18n.Message{
|
||||
ID: "ClearFilePanel",
|
||||
Other: "maak bestandsvenster leeg",
|
||||
}, &i18n.Message{
|
||||
ID: "MergeAborted",
|
||||
Other: "Merge afgebroken",
|
||||
@@ -382,9 +379,6 @@ func addDutch(i18nObject *i18n.Bundle) error {
|
||||
}, &i18n.Message{
|
||||
ID: "GitconfigParseErr",
|
||||
Other: `Gogit kon je gitconfig bestand niet goed parsen door de aanwezigheid van losstaande '\' tekens. Het weghalen van deze tekens zou het probleem moeten oplossen. `,
|
||||
}, &i18n.Message{
|
||||
ID: "removeFile",
|
||||
Other: `Verwijder als untracked / uitchecken wordt gevolgd (ga weg)`,
|
||||
}, &i18n.Message{
|
||||
ID: "editFile",
|
||||
Other: `verander bestand`,
|
||||
@@ -397,9 +391,6 @@ func addDutch(i18nObject *i18n.Bundle) error {
|
||||
}, &i18n.Message{
|
||||
ID: "refreshFiles",
|
||||
Other: `refresh bestanden`,
|
||||
}, &i18n.Message{
|
||||
ID: "resetHard",
|
||||
Other: `harde reset and verwijderen ongevolgde bestanden`,
|
||||
}, &i18n.Message{
|
||||
ID: "mergeIntoCurrentBranch",
|
||||
Other: `merge in met huidige checked out branch`,
|
||||
@@ -423,7 +414,7 @@ func addDutch(i18nObject *i18n.Bundle) error {
|
||||
Other: `fetch`,
|
||||
}, &i18n.Message{
|
||||
ID: "NoAutomaticGitFetchTitle",
|
||||
Other: `Geen automatiese git fetch`,
|
||||
Other: `Geen automatische git fetch`,
|
||||
}, &i18n.Message{
|
||||
ID: "NoAutomaticGitFetchBody",
|
||||
Other: `Lazygit kan niet "git fetch" uitvoeren in een privé repository, gebruik f in het branches paneel om "git fetch" manueel uit te voeren`,
|
||||
@@ -443,7 +434,7 @@ func addDutch(i18nObject *i18n.Bundle) error {
|
||||
ID: "StageLine",
|
||||
Other: `stage lijn`,
|
||||
}, &i18n.Message{
|
||||
ID: "EscapeStaging",
|
||||
ID: "ReturnToFilesPanel",
|
||||
Other: `ga terug naar het bestanden paneel`,
|
||||
}, &i18n.Message{
|
||||
ID: "CantFindHunks",
|
||||
@@ -459,40 +450,34 @@ func addDutch(i18nObject *i18n.Bundle) error {
|
||||
Other: "Merging",
|
||||
}, &i18n.Message{
|
||||
ID: "ConfirmRebase",
|
||||
Other: "Are you sure you want to rebase {{.checkedOutBranch}} onto {{.selectedBranch}}?",
|
||||
Other: "Weet je zeker dat je {{.checkedOutBranch}} op {{.selectedBranch}} wil rebasen?",
|
||||
}, &i18n.Message{
|
||||
ID: "ConfirmMerge",
|
||||
Other: "Are you sure you want to merge {{.selectedBranch}} into {{.checkedOutBranch}}?",
|
||||
Other: "Weet je zeker dat je {{.selectedBranch}} in {{.checkedOutBranch}} wil mergen?",
|
||||
}, &i18n.Message{
|
||||
ID: "FwdNoUpstream",
|
||||
Other: "Cannot fast-forward a branch with no upstream",
|
||||
Other: "Kan niet de branch vooruitspoelen zonder upstream",
|
||||
}, &i18n.Message{
|
||||
ID: "ErrorOccurred",
|
||||
Other: "An error occurred! Please create an issue at https://github.com/jesseduffield/lazygit/issues",
|
||||
Other: "Er is iets fout gegaan! Zou je hier een issue aan willen maken: https://github.com/jesseduffield/lazygit/issues",
|
||||
}, &i18n.Message{
|
||||
ID: "FwdCommitsToPush",
|
||||
Other: "Cannot fast-forward a branch with commits to push",
|
||||
Other: "Je kan niet vooruitspoelen als de branch geen nieuwe commits heeft",
|
||||
}, &i18n.Message{
|
||||
ID: "MainTitle",
|
||||
Other: "Main",
|
||||
Other: "Hoofd",
|
||||
}, &i18n.Message{
|
||||
ID: "NormalTitle",
|
||||
Other: "Normal",
|
||||
Other: "Normaal",
|
||||
}, &i18n.Message{
|
||||
ID: "softReset",
|
||||
Other: "soft reset to last commit",
|
||||
}, &i18n.Message{
|
||||
ID: "SoftReset",
|
||||
Other: "Soft reset",
|
||||
}, &i18n.Message{
|
||||
ID: "ConfirmSoftReset",
|
||||
Other: "Are you sure you want to `reset --soft HEAD^`? The changes in your topmost commit will be placed in your working tree",
|
||||
Other: "zacht reset",
|
||||
}, &i18n.Message{
|
||||
ID: "CantRebaseOntoSelf",
|
||||
Other: "You cannot rebase a branch onto itself",
|
||||
Other: "Je kan niet een branch rebasen op zichzelf",
|
||||
}, &i18n.Message{
|
||||
ID: "SureSquashThisCommit",
|
||||
Other: "Are you sure you want to squash this commit into the commit below?",
|
||||
Other: "Weet je zeker dat je deze commit wil samenvoegen met de commit hieronder?",
|
||||
}, &i18n.Message{
|
||||
ID: "Squash",
|
||||
Other: "Squash",
|
||||
@@ -501,127 +486,130 @@ func addDutch(i18nObject *i18n.Bundle) error {
|
||||
Other: "pick commit (when mid-rebase)",
|
||||
}, &i18n.Message{
|
||||
ID: "revertCommit",
|
||||
Other: "revert commit",
|
||||
Other: "commit omgedaan maken",
|
||||
}, &i18n.Message{
|
||||
ID: "deleteCommit",
|
||||
Other: "delete commit",
|
||||
Other: "verwijder commit",
|
||||
}, &i18n.Message{
|
||||
ID: "moveDownCommit",
|
||||
Other: "move commit down one",
|
||||
Other: "verplaats commit 1 omlaag",
|
||||
}, &i18n.Message{
|
||||
ID: "moveUpCommit",
|
||||
Other: "move commit up one",
|
||||
Other: "verplaats commit 1 omhoog",
|
||||
}, &i18n.Message{
|
||||
ID: "editCommit",
|
||||
Other: "edit commit",
|
||||
Other: "verander commit",
|
||||
}, &i18n.Message{
|
||||
ID: "amendToCommit",
|
||||
Other: "amend commit with staged changes",
|
||||
Other: "wijzig commit met staged veranderingen",
|
||||
}, &i18n.Message{
|
||||
ID: "FoundConflicts",
|
||||
Other: "Damn, conflicts! To abort press 'esc', otherwise press 'enter'",
|
||||
Other: "Conflicten!, Om af te breken druk 'esc', anders druk op 'enter'",
|
||||
}, &i18n.Message{
|
||||
ID: "FoundConflictsTitle",
|
||||
Other: "Auto-merge failed",
|
||||
Other: "Auto-merge mislukt",
|
||||
}, &i18n.Message{
|
||||
ID: "Undo",
|
||||
Other: "undo",
|
||||
Other: "ongedaan maken",
|
||||
}, &i18n.Message{
|
||||
ID: "PickHunk",
|
||||
Other: "pick hunk",
|
||||
}, &i18n.Message{
|
||||
ID: "PickBothHunks",
|
||||
Other: "pick both hunks",
|
||||
Other: "pick beide hunks",
|
||||
}, &i18n.Message{
|
||||
ID: "ViewMergeRebaseOptions",
|
||||
Other: "view merge/rebase options",
|
||||
Other: "bekijk merge/rebase opties",
|
||||
}, &i18n.Message{
|
||||
ID: "NotMergingOrRebasing",
|
||||
Other: "You are currently neither rebasing nor merging",
|
||||
Other: "Je bent momenteel niet aan het rebasen of mergen",
|
||||
}, &i18n.Message{
|
||||
ID: "RecentRepos",
|
||||
Other: "recent repositories",
|
||||
}, &i18n.Message{
|
||||
ID: "MergeOptionsTitle",
|
||||
Other: "Merge Options",
|
||||
Other: "Merge Opties",
|
||||
}, &i18n.Message{
|
||||
ID: "RebaseOptionsTitle",
|
||||
Other: "Rebase Options",
|
||||
Other: "Rebase Opties",
|
||||
}, &i18n.Message{
|
||||
ID: "ConflictsResolved",
|
||||
Other: "all merge conflicts resolved. Continue?",
|
||||
Other: "alle merge conflicten zijn opgelost. Wilt je verder gaan?",
|
||||
}, &i18n.Message{
|
||||
ID: "NoRoom",
|
||||
Other: "Not enough room",
|
||||
Other: "Niet genoeg ruimte",
|
||||
}, &i18n.Message{
|
||||
ID: "YouAreHere",
|
||||
Other: "YOU ARE HERE",
|
||||
Other: "JE BENT HIER",
|
||||
}, &i18n.Message{
|
||||
ID: "rewordNotSupported",
|
||||
Other: "rewording commits while interactively rebasing is not currently supported",
|
||||
Other: "herformatteren van commits in interactief rebasen is nog niet ondersteund",
|
||||
}, &i18n.Message{
|
||||
ID: "cherryPickCopy",
|
||||
Other: "copy commit (cherry-pick)",
|
||||
Other: "kopiëer commit (cherry-pick)",
|
||||
}, &i18n.Message{
|
||||
ID: "cherryPickCopyRange",
|
||||
Other: "copy commit range (cherry-pick)",
|
||||
Other: "kopiëer commit reeks (cherry-pick)",
|
||||
}, &i18n.Message{
|
||||
ID: "pasteCommits",
|
||||
Other: "paste commits (cherry-pick)",
|
||||
Other: "plak commits (cherry-pick)",
|
||||
}, &i18n.Message{
|
||||
ID: "SureCherryPick",
|
||||
Other: "Are you sure you want to cherry-pick the copied commits onto this branch?",
|
||||
Other: "Weet je zeker dat je de gekopieerde commits naar deze branch wil cherry-picken?",
|
||||
}, &i18n.Message{
|
||||
ID: "CherryPick",
|
||||
Other: "Cherry-Pick",
|
||||
}, &i18n.Message{
|
||||
ID: "CannotRebaseOntoFirstCommit",
|
||||
Other: "You cannot interactive rebase onto the first commit",
|
||||
Other: "Je kan niet interactief rebasen naar de eerste commit",
|
||||
}, &i18n.Message{
|
||||
ID: "CannotSquashOntoSecondCommit",
|
||||
Other: "Je kan niet een squash/fixup doen naar de 2de commit",
|
||||
}, &i18n.Message{
|
||||
ID: "Donate",
|
||||
Other: "Donate",
|
||||
Other: "Doneer",
|
||||
}, &i18n.Message{
|
||||
ID: "PrevLine",
|
||||
Other: "select previous line",
|
||||
Other: "selecteer de vorige lijn",
|
||||
}, &i18n.Message{
|
||||
ID: "NextLine",
|
||||
Other: "select next line",
|
||||
Other: "selecteer de volgende lijn",
|
||||
}, &i18n.Message{
|
||||
ID: "PrevHunk",
|
||||
Other: "select previous hunk",
|
||||
Other: "selecteer de vorige hunk",
|
||||
}, &i18n.Message{
|
||||
ID: "NextHunk",
|
||||
Other: "select next hunk",
|
||||
Other: "selecteer de volgende hunk",
|
||||
}, &i18n.Message{
|
||||
ID: "PrevConflict",
|
||||
Other: "select previous conflict",
|
||||
Other: "selecteer voorgaand conflict",
|
||||
}, &i18n.Message{
|
||||
ID: "NextConflict",
|
||||
Other: "select next conflict",
|
||||
Other: "selecteer volgende conflict",
|
||||
}, &i18n.Message{
|
||||
ID: "SelectTop",
|
||||
Other: "select top hunk",
|
||||
Other: "selecteer bovenste hunk",
|
||||
}, &i18n.Message{
|
||||
ID: "SelectBottom",
|
||||
Other: "select bottom hunk",
|
||||
Other: "selecteer onderste hunk",
|
||||
}, &i18n.Message{
|
||||
ID: "ScrollDown",
|
||||
Other: "scroll down",
|
||||
Other: "scroll omlaag",
|
||||
}, &i18n.Message{
|
||||
ID: "ScrollUp",
|
||||
Other: "scroll up",
|
||||
Other: "scroll omhoog",
|
||||
}, &i18n.Message{
|
||||
ID: "AmendCommitTitle",
|
||||
Other: "Amend Commit",
|
||||
Other: "Commit wijzigen",
|
||||
}, &i18n.Message{
|
||||
ID: "AmendCommitPrompt",
|
||||
Other: "Are you sure you want to amend this commit with your staged files?",
|
||||
Other: "Weet je zeker dat je deze commit wil wijzigen met de vorige staged bestanden?",
|
||||
}, &i18n.Message{
|
||||
ID: "DeleteCommitTitle",
|
||||
Other: "Delete Commit",
|
||||
Other: "Verwijder Commit",
|
||||
}, &i18n.Message{
|
||||
ID: "DeleteCommitPrompt",
|
||||
Other: "Are you sure you want to delete this commit?",
|
||||
Other: "Weet je zeker dat je deze commit wil verwijderen?",
|
||||
}, &i18n.Message{
|
||||
ID: "SquashingStatus",
|
||||
Other: "squashing",
|
||||
@@ -630,19 +618,148 @@ func addDutch(i18nObject *i18n.Bundle) error {
|
||||
Other: "fixing up",
|
||||
}, &i18n.Message{
|
||||
ID: "DeletingStatus",
|
||||
Other: "deleting",
|
||||
Other: "verwijderen",
|
||||
}, &i18n.Message{
|
||||
ID: "MovingStatus",
|
||||
Other: "moving",
|
||||
Other: "verplaatsen",
|
||||
}, &i18n.Message{
|
||||
ID: "RebasingStatus",
|
||||
Other: "rebasing",
|
||||
}, &i18n.Message{
|
||||
ID: "AmendingStatus",
|
||||
Other: "amending",
|
||||
Other: "wijzigen",
|
||||
}, &i18n.Message{
|
||||
ID: "CherryPickingStatus",
|
||||
Other: "cherry-picking",
|
||||
}, &i18n.Message{
|
||||
ID: "CommitFiles",
|
||||
Other: "Commit bestanden",
|
||||
}, &i18n.Message{
|
||||
ID: "viewCommitFiles",
|
||||
Other: "bekijk gecommite bestanden",
|
||||
}, &i18n.Message{
|
||||
ID: "CommitFilesTitle",
|
||||
Other: "Commit bestanden",
|
||||
}, &i18n.Message{
|
||||
ID: "goBack",
|
||||
Other: "ga terug",
|
||||
}, &i18n.Message{
|
||||
ID: "NoCommiteFiles",
|
||||
Other: "Geen bestanden voor deze commit",
|
||||
}, &i18n.Message{
|
||||
ID: "checkoutCommitFile",
|
||||
Other: "bestand uitchecken",
|
||||
}, &i18n.Message{
|
||||
ID: "discardOldFileChange",
|
||||
Other: "uitsluit deze commit zijn veranderingen aan dit bestand",
|
||||
}, &i18n.Message{
|
||||
ID: "DiscardFileChangesTitle",
|
||||
Other: "uitsluit bestand zijn veranderingen",
|
||||
}, &i18n.Message{
|
||||
ID: "DiscardFileChangesPrompt",
|
||||
Other: "Weet je zeker dat je de wijzigingen van deze commit in dit bestand wilt weggooien? Als dit bestand is gecreëerd in deze commit dan zal dit bestand worden verwijdert",
|
||||
}, &i18n.Message{
|
||||
ID: "DisabledForGPG",
|
||||
Other: "Onderdelen niet beschikbaar voor gebruikers die GPG gebruiken",
|
||||
}, &i18n.Message{
|
||||
ID: "CreateRepo",
|
||||
Other: "Niet in een git repository. Creëer een nieuwe git repository? (y/n): ",
|
||||
}, &i18n.Message{
|
||||
ID: "AutoStashTitle",
|
||||
Other: "Autostash?",
|
||||
}, &i18n.Message{
|
||||
ID: "AutoStashPrompt",
|
||||
Other: "Je moet je veranderingen stashen en poppen om ze over te bregen. Dit automatisch doen? (enter/esc)",
|
||||
}, &i18n.Message{
|
||||
ID: "StashPrefix",
|
||||
Other: "Auto-stashing veranderingen voor ",
|
||||
}, &i18n.Message{
|
||||
ID: "viewDiscardOptions",
|
||||
Other: "bekijk 'veranderingen ongedaan maken' opties",
|
||||
}, &i18n.Message{
|
||||
ID: "cancel",
|
||||
Other: "anuleren",
|
||||
}, &i18n.Message{
|
||||
ID: "discardAllChanges",
|
||||
Other: "negeer alle wijzigingen",
|
||||
}, &i18n.Message{
|
||||
ID: "discardUnstagedChanges",
|
||||
Other: "negeer unstaged wijzigingen",
|
||||
}, &i18n.Message{
|
||||
ID: "discardAllChangesToAllFiles",
|
||||
Other: "verwijder werkende tree",
|
||||
}, &i18n.Message{
|
||||
ID: "discardAnyUnstagedChanges",
|
||||
Other: "discard unstaged changes",
|
||||
}, &i18n.Message{
|
||||
ID: "discardUntrackedFiles",
|
||||
Other: "negeer niet-gevonden bestanden",
|
||||
}, &i18n.Message{
|
||||
ID: "viewResetOptions",
|
||||
Other: `bekijk reset opties`,
|
||||
}, &i18n.Message{
|
||||
ID: "hardReset",
|
||||
Other: "harde reset",
|
||||
}, &i18n.Message{
|
||||
ID: "createFixupCommit",
|
||||
Other: `creëer fixup commit voor deze commit`,
|
||||
}, &i18n.Message{
|
||||
ID: "squashAboveCommits",
|
||||
Other: `squash bovenstaande commits`,
|
||||
}, &i18n.Message{
|
||||
ID: "SquashAboveCommits",
|
||||
Other: `Squash bovenstaande commits`,
|
||||
}, &i18n.Message{
|
||||
ID: "SureSquashAboveCommits",
|
||||
Other: `Weet je zeker dat je alles wil squash/fixup! voor de bovenstaand commits {{.commit}}?`,
|
||||
}, &i18n.Message{
|
||||
ID: "CreateFixupCommit",
|
||||
Other: `Creëer fixup commit`,
|
||||
}, &i18n.Message{
|
||||
ID: "SureCreateFixupCommit",
|
||||
Other: `Weet je zeker dat je een fixup wil maken! commit voor commit {{.commit}}?`,
|
||||
}, &i18n.Message{
|
||||
ID: "executeCustomCommand",
|
||||
Other: "voor aangepast commando uit",
|
||||
}, &i18n.Message{
|
||||
ID: "CustomCommand",
|
||||
Other: "Aangepast commando:",
|
||||
}, &i18n.Message{
|
||||
ID: "commitChangesWithoutHook",
|
||||
Other: "commit veranderingen zonder pre-commit hook",
|
||||
}, &i18n.Message{
|
||||
ID: "SkipHookPrefixNotConfigured",
|
||||
Other: "Je hebt nog niet een commit bericht voorvoegsel ingesteld voor het overslaan van hooks. Set `git.skipHookPrefix = 'WIP'` in je config",
|
||||
}, &i18n.Message{
|
||||
ID: "resetTo",
|
||||
Other: `reset to`,
|
||||
}, &i18n.Message{
|
||||
ID: "pressEnterToReturn",
|
||||
Other: "Press enter to return to lazygit",
|
||||
}, &i18n.Message{
|
||||
ID: "viewStashOptions",
|
||||
Other: "view stash options",
|
||||
}, &i18n.Message{
|
||||
ID: "stashAllChanges",
|
||||
Other: "stash-bestanden",
|
||||
}, &i18n.Message{
|
||||
ID: "stashStagedChanges",
|
||||
Other: "stash staged changes",
|
||||
}, &i18n.Message{
|
||||
ID: "stashOptions",
|
||||
Other: "Stash options",
|
||||
}, &i18n.Message{
|
||||
ID: "notARepository",
|
||||
Other: "Error: must be run inside a git repository",
|
||||
}, &i18n.Message{
|
||||
ID: "jump",
|
||||
Other: "jump to panel",
|
||||
}, &i18n.Message{
|
||||
ID: "ExitLineByLineMode",
|
||||
Other: `exit line-by-line mode`,
|
||||
}, &i18n.Message{
|
||||
ID: "EnterUpstream",
|
||||
Other: `Enter upstream as '<remote> <branchname>'`,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
+206
-35
@@ -36,12 +36,24 @@ func addEnglish(i18nObject *i18n.Bundle) error {
|
||||
}, &i18n.Message{
|
||||
ID: "CommitsTitle",
|
||||
Other: "Commits",
|
||||
}, &i18n.Message{
|
||||
ID: "CommitsDiffTitle",
|
||||
Other: "Commits (specific diff mode)",
|
||||
}, &i18n.Message{
|
||||
ID: "CommitsDiff",
|
||||
Other: "select commit to diff with another commit",
|
||||
}, &i18n.Message{
|
||||
ID: "StashTitle",
|
||||
Other: "Stash",
|
||||
}, &i18n.Message{
|
||||
ID: "StagingMainTitle",
|
||||
Other: `Stage Lines/Hunks`,
|
||||
ID: "UnstagedChanges",
|
||||
Other: `Unstaged Changes`,
|
||||
}, &i18n.Message{
|
||||
ID: "StagedChanges",
|
||||
Other: `Staged Changes`,
|
||||
}, &i18n.Message{
|
||||
ID: "PatchBuildingMainTitle",
|
||||
Other: `Add Lines/Hunks To Patch`,
|
||||
}, &i18n.Message{
|
||||
ID: "MergingMainTitle",
|
||||
Other: "Resolve merge conflicts",
|
||||
@@ -99,12 +111,6 @@ func addEnglish(i18nObject *i18n.Bundle) error {
|
||||
}, &i18n.Message{
|
||||
ID: "execute",
|
||||
Other: "execute",
|
||||
}, &i18n.Message{
|
||||
ID: "stashFiles",
|
||||
Other: "stash files",
|
||||
}, &i18n.Message{
|
||||
ID: "softReset",
|
||||
Other: "soft reset to last commit",
|
||||
}, &i18n.Message{
|
||||
ID: "open",
|
||||
Other: "open",
|
||||
@@ -129,9 +135,6 @@ func addEnglish(i18nObject *i18n.Bundle) error {
|
||||
}, &i18n.Message{
|
||||
ID: "pull",
|
||||
Other: "pull",
|
||||
}, &i18n.Message{
|
||||
ID: "addPatch",
|
||||
Other: "add patch",
|
||||
}, &i18n.Message{
|
||||
ID: "edit",
|
||||
Other: "edit",
|
||||
@@ -181,14 +184,8 @@ func addEnglish(i18nObject *i18n.Bundle) error {
|
||||
ID: "FileNoMergeCons",
|
||||
Other: "This file has no inline merge conflicts",
|
||||
}, &i18n.Message{
|
||||
ID: "SureResetHardHead",
|
||||
Other: "Are you sure you want to `reset --hard HEAD` and `clean -fd`? You may lose changes",
|
||||
}, &i18n.Message{
|
||||
ID: "SoftReset",
|
||||
Other: "Soft reset",
|
||||
}, &i18n.Message{
|
||||
ID: "ConfirmSoftReset",
|
||||
Other: "Are you sure you want to `reset --soft HEAD^`? The changes in your topmost commit will be placed in your working tree",
|
||||
ID: "softReset",
|
||||
Other: "soft reset",
|
||||
}, &i18n.Message{
|
||||
ID: "SureTo",
|
||||
Other: "Are you sure you want to {{.deleteVerb}} {{.fileName}} (you will lose your changes)?",
|
||||
@@ -405,9 +402,6 @@ func addEnglish(i18nObject *i18n.Bundle) error {
|
||||
}, &i18n.Message{
|
||||
ID: "NoChangedFiles",
|
||||
Other: "No changed files",
|
||||
}, &i18n.Message{
|
||||
ID: "ClearFilePanel",
|
||||
Other: "Clear file panel",
|
||||
}, &i18n.Message{
|
||||
ID: "MergeAborted",
|
||||
Other: "Merge aborted",
|
||||
@@ -444,12 +438,22 @@ func addEnglish(i18nObject *i18n.Bundle) error {
|
||||
}, &i18n.Message{
|
||||
ID: "AnonymousReportingPrompt",
|
||||
Other: "Would you like to enable anonymous reporting data to help improve lazygit? (enter/esc)",
|
||||
}, &i18n.Message{
|
||||
ID: "ShamelessSelfPromotionTitle",
|
||||
Other: "Shameless Self Promotion",
|
||||
}, &i18n.Message{
|
||||
ID: "ShamelessSelfPromotionMessage",
|
||||
Other: `Thanks for using lazygit! Three things to share with you:
|
||||
|
||||
1) lazygit now has basic mouse support!
|
||||
|
||||
2) If you want to learn about lazygit's features, watch this vid:
|
||||
https://youtu.be/CPLdltN7wgE
|
||||
|
||||
3) Github are now matching any donations dollar-for-dollar for the next 12 months, so if you've been tossing up over whether to click the donate link in the bottom right corner, now is the time!`,
|
||||
}, &i18n.Message{
|
||||
ID: "GitconfigParseErr",
|
||||
Other: `Gogit failed to parse your gitconfig file due to the presence of unquoted '\' characters. Removing these should fix the issue.`,
|
||||
}, &i18n.Message{
|
||||
ID: "removeFile",
|
||||
Other: `delete if untracked / checkout if tracked`,
|
||||
}, &i18n.Message{
|
||||
ID: "editFile",
|
||||
Other: `edit file`,
|
||||
@@ -462,9 +466,6 @@ func addEnglish(i18nObject *i18n.Bundle) error {
|
||||
}, &i18n.Message{
|
||||
ID: "refreshFiles",
|
||||
Other: `refresh files`,
|
||||
}, &i18n.Message{
|
||||
ID: "resetHard",
|
||||
Other: `reset hard and remove untracked files`,
|
||||
}, &i18n.Message{
|
||||
ID: "mergeIntoCurrentBranch",
|
||||
Other: `merge into currently checked out branch`,
|
||||
@@ -497,15 +498,32 @@ func addEnglish(i18nObject *i18n.Bundle) error {
|
||||
Other: `stage individual hunks/lines`,
|
||||
}, &i18n.Message{
|
||||
ID: "FileStagingRequirements",
|
||||
Other: `Can only stage individual lines for tracked files with unstaged changes`,
|
||||
Other: `Can only stage individual lines for tracked files`,
|
||||
}, &i18n.Message{
|
||||
ID: "StageHunk",
|
||||
Other: `stage hunk`,
|
||||
ID: "SelectHunk",
|
||||
Other: `select hunk`,
|
||||
}, &i18n.Message{
|
||||
ID: "StageLine",
|
||||
Other: `stage line`,
|
||||
ID: "StageSelection",
|
||||
Other: `stage selection`,
|
||||
}, &i18n.Message{
|
||||
ID: "EscapeStaging",
|
||||
ID: "ResetSelection",
|
||||
Other: `reset selection`,
|
||||
}, &i18n.Message{
|
||||
ID: "ToggleDragSelect",
|
||||
Other: `toggle drag select`,
|
||||
}, &i18n.Message{
|
||||
ID: "ToggleSelectHunk",
|
||||
Other: `toggle select hunk`,
|
||||
},
|
||||
&i18n.Message{
|
||||
ID: "TogglePanel",
|
||||
Other: `switch to other panel`,
|
||||
},
|
||||
&i18n.Message{
|
||||
ID: "CantStageStaged",
|
||||
Other: `You can't stage an already staged change!`,
|
||||
}, &i18n.Message{
|
||||
ID: "ReturnToFilesPanel",
|
||||
Other: `return to files panel`,
|
||||
}, &i18n.Message{
|
||||
ID: "CantFindHunks",
|
||||
@@ -521,7 +539,7 @@ func addEnglish(i18nObject *i18n.Bundle) error {
|
||||
Other: "fetching and fast-forwarding {{.from}} -> {{.to}} ...",
|
||||
}, &i18n.Message{
|
||||
ID: "FoundConflicts",
|
||||
Other: "Damn, conflicts! To abort press 'esc', otherwise press 'enter'",
|
||||
Other: "Conflicts! To abort press 'esc', otherwise press 'enter'",
|
||||
}, &i18n.Message{
|
||||
ID: "FoundConflictsTitle",
|
||||
Other: "Auto-merge failed",
|
||||
@@ -600,6 +618,9 @@ func addEnglish(i18nObject *i18n.Bundle) error {
|
||||
}, &i18n.Message{
|
||||
ID: "CannotRebaseOntoFirstCommit",
|
||||
Other: "You cannot interactive rebase onto the first commit",
|
||||
}, &i18n.Message{
|
||||
ID: "CannotSquashOntoSecondCommit",
|
||||
Other: "You cannot squash/fixup onto the second commit",
|
||||
}, &i18n.Message{
|
||||
ID: "Donate",
|
||||
Other: "Donate",
|
||||
@@ -666,6 +687,156 @@ func addEnglish(i18nObject *i18n.Bundle) error {
|
||||
}, &i18n.Message{
|
||||
ID: "CherryPickingStatus",
|
||||
Other: "cherry-picking",
|
||||
}, &i18n.Message{
|
||||
ID: "CommitFiles",
|
||||
Other: "Commit files",
|
||||
}, &i18n.Message{
|
||||
ID: "viewCommitFiles",
|
||||
Other: "view commit's files",
|
||||
}, &i18n.Message{
|
||||
ID: "CommitFilesTitle",
|
||||
Other: "Commit files",
|
||||
}, &i18n.Message{
|
||||
ID: "goBack",
|
||||
Other: "go back",
|
||||
}, &i18n.Message{
|
||||
ID: "NoCommiteFiles",
|
||||
Other: "No files for this commit",
|
||||
}, &i18n.Message{
|
||||
ID: "checkoutCommitFile",
|
||||
Other: "checkout file",
|
||||
}, &i18n.Message{
|
||||
ID: "discardOldFileChange",
|
||||
Other: "discard this commit's changes to this file",
|
||||
}, &i18n.Message{
|
||||
ID: "DiscardFileChangesTitle",
|
||||
Other: "Discard file changes",
|
||||
}, &i18n.Message{
|
||||
ID: "DiscardFileChangesPrompt",
|
||||
Other: "Are you sure you want to discard this commit's changes to this file? If this file was created in this commit, it will be deleted",
|
||||
}, &i18n.Message{
|
||||
ID: "DisabledForGPG",
|
||||
Other: "Feature not available for users using GPG",
|
||||
}, &i18n.Message{
|
||||
ID: "CreateRepo",
|
||||
Other: "Not in a git repository. Create a new git repository? (y/n): ",
|
||||
}, &i18n.Message{
|
||||
ID: "AutoStashTitle",
|
||||
Other: "Autostash?",
|
||||
}, &i18n.Message{
|
||||
ID: "AutoStashPrompt",
|
||||
Other: "You must stash and pop your changes to bring them across. Do this automatically? (enter/esc)",
|
||||
}, &i18n.Message{
|
||||
ID: "StashPrefix",
|
||||
Other: "Auto-stashing changes for ",
|
||||
}, &i18n.Message{
|
||||
ID: "viewDiscardOptions",
|
||||
Other: "view 'discard changes' options",
|
||||
}, &i18n.Message{
|
||||
ID: "cancel",
|
||||
Other: "cancel",
|
||||
}, &i18n.Message{
|
||||
ID: "discardAllChanges",
|
||||
Other: "discard all changes",
|
||||
}, &i18n.Message{
|
||||
ID: "discardUnstagedChanges",
|
||||
Other: "discard unstaged changes",
|
||||
}, &i18n.Message{
|
||||
ID: "discardAllChangesToAllFiles",
|
||||
Other: "nuke working tree",
|
||||
}, &i18n.Message{
|
||||
ID: "discardAnyUnstagedChanges",
|
||||
Other: "discard unstaged changes",
|
||||
}, &i18n.Message{
|
||||
ID: "discardUntrackedFiles",
|
||||
Other: "discard untracked files",
|
||||
}, &i18n.Message{
|
||||
ID: "hardReset",
|
||||
Other: "hard reset",
|
||||
}, &i18n.Message{
|
||||
ID: "viewResetOptions",
|
||||
Other: `view reset options`,
|
||||
}, &i18n.Message{
|
||||
ID: "createFixupCommit",
|
||||
Other: `create fixup commit for this commit`,
|
||||
}, &i18n.Message{
|
||||
ID: "squashAboveCommits",
|
||||
Other: `squash above commits`,
|
||||
}, &i18n.Message{
|
||||
ID: "SquashAboveCommits",
|
||||
Other: `Squash above commits`,
|
||||
}, &i18n.Message{
|
||||
ID: "SureSquashAboveCommits",
|
||||
Other: `Are you sure you want to squash all fixup! commits above {{.commit}}?`,
|
||||
}, &i18n.Message{
|
||||
ID: "CreateFixupCommit",
|
||||
Other: `Create fixup commit`,
|
||||
}, &i18n.Message{
|
||||
ID: "SureCreateFixupCommit",
|
||||
Other: `Are you sure you want to create a fixup! commit for commit {{.commit}}?`,
|
||||
}, &i18n.Message{
|
||||
ID: "executeCustomCommand",
|
||||
Other: "execute custom command",
|
||||
}, &i18n.Message{
|
||||
ID: "CustomCommand",
|
||||
Other: "Custom Command:",
|
||||
}, &i18n.Message{
|
||||
ID: "commitChangesWithoutHook",
|
||||
Other: "commit changes without pre-commit hook",
|
||||
}, &i18n.Message{
|
||||
ID: "SkipHookPrefixNotConfigured",
|
||||
Other: "You have not configured a commit message prefix for skipping hooks. Set `git.skipHookPrefix = 'WIP'` in your config",
|
||||
}, &i18n.Message{
|
||||
ID: "resetTo",
|
||||
Other: `reset to`,
|
||||
}, &i18n.Message{
|
||||
ID: "pressEnterToReturn",
|
||||
Other: "Press enter to return to lazygit",
|
||||
}, &i18n.Message{
|
||||
ID: "viewStashOptions",
|
||||
Other: "view stash options",
|
||||
}, &i18n.Message{
|
||||
ID: "stashAllChanges",
|
||||
Other: "stash changes",
|
||||
}, &i18n.Message{
|
||||
ID: "stashStagedChanges",
|
||||
Other: "stash staged changes",
|
||||
}, &i18n.Message{
|
||||
ID: "stashOptions",
|
||||
Other: "Stash options",
|
||||
}, &i18n.Message{
|
||||
ID: "notARepository",
|
||||
Other: "Error: must be run inside a git repository",
|
||||
}, &i18n.Message{
|
||||
ID: "jump",
|
||||
Other: "jump to panel",
|
||||
}, &i18n.Message{
|
||||
ID: "DiscardPatch",
|
||||
Other: "Discard Patch",
|
||||
}, &i18n.Message{
|
||||
ID: "DiscardPatchConfirm",
|
||||
Other: "You can only build a patch from one commit at a time. Discard current patch?",
|
||||
}, &i18n.Message{
|
||||
ID: "CantPatchWhileRebasingError",
|
||||
Other: "You cannot build a patch or run patch commands while in a merging or rebasing state",
|
||||
}, &i18n.Message{
|
||||
ID: "toggleAddToPatch",
|
||||
Other: "toggle file included in patch",
|
||||
}, &i18n.Message{
|
||||
ID: "PatchOptionsTitle",
|
||||
Other: "Patch Options",
|
||||
}, &i18n.Message{
|
||||
ID: "NoPatchError",
|
||||
Other: "No patch created yet. To start building a patch, use 'space' on a commit file or enter to add specific lines",
|
||||
}, &i18n.Message{
|
||||
ID: "enterFile",
|
||||
Other: "enter file to add selected lines to the patch",
|
||||
}, &i18n.Message{
|
||||
ID: "ExitLineByLineMode",
|
||||
Other: `exit line-by-line mode`,
|
||||
}, &i18n.Message{
|
||||
ID: "EnterUpstream",
|
||||
Other: `Enter upstream as '<remote> <branchname>'`,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
+1
-1
@@ -87,7 +87,7 @@ func detectLanguage(langDetector func() (string, error)) string {
|
||||
// setupLocalizer creates a new localizer using given userLang
|
||||
func setupLocalizer(log *logrus.Entry, userLang string) *Localizer {
|
||||
// create a i18n bundle that can be used to add translations and other things
|
||||
i18nBundle := &i18n.Bundle{DefaultLanguage: language.English}
|
||||
i18nBundle := i18n.NewBundle(language.English)
|
||||
|
||||
addBundles(log, i18nBundle)
|
||||
|
||||
|
||||
+146
-29
@@ -26,12 +26,21 @@ func addPolish(i18nObject *i18n.Bundle) error {
|
||||
}, &i18n.Message{
|
||||
ID: "CommitsTitle",
|
||||
Other: "Commity",
|
||||
}, &i18n.Message{
|
||||
ID: "CommitsDiffTitle",
|
||||
Other: "Commits (specific diff mode)",
|
||||
}, &i18n.Message{
|
||||
ID: "CommitsDiff",
|
||||
Other: "select commit to diff with another commit",
|
||||
}, &i18n.Message{
|
||||
ID: "StashTitle",
|
||||
Other: "Schowek",
|
||||
}, &i18n.Message{
|
||||
ID: "StagingMainTitle",
|
||||
Other: `Stage Lines/Hunks`,
|
||||
ID: "UnstagedChanges",
|
||||
Other: `Unstaged Changes`,
|
||||
}, &i18n.Message{
|
||||
ID: "StagedChanges",
|
||||
Other: `Staged Changes`,
|
||||
}, &i18n.Message{
|
||||
ID: "MergingMainTitle",
|
||||
Other: "Resolve merge conflicts",
|
||||
@@ -77,9 +86,6 @@ func addPolish(i18nObject *i18n.Bundle) error {
|
||||
}, &i18n.Message{
|
||||
ID: "execute",
|
||||
Other: "wykonaj",
|
||||
}, &i18n.Message{
|
||||
ID: "stashFiles",
|
||||
Other: "przechowaj pliki",
|
||||
}, &i18n.Message{
|
||||
ID: "open",
|
||||
Other: "otwórz",
|
||||
@@ -98,9 +104,6 @@ func addPolish(i18nObject *i18n.Bundle) error {
|
||||
}, &i18n.Message{
|
||||
ID: "refresh",
|
||||
Other: "odśwież",
|
||||
}, &i18n.Message{
|
||||
ID: "addPatch",
|
||||
Other: "dodaj łatkę",
|
||||
}, &i18n.Message{
|
||||
ID: "edit",
|
||||
Other: "edytuj",
|
||||
@@ -146,9 +149,6 @@ func addPolish(i18nObject *i18n.Bundle) error {
|
||||
}, &i18n.Message{
|
||||
ID: "FileNoMergeCons",
|
||||
Other: "Ten plik nie powoduje konfliktów scalania",
|
||||
}, &i18n.Message{
|
||||
ID: "SureResetHardHead",
|
||||
Other: "Jesteś pewny, że chcesz wykonać `reset --hard HEAD` i `clean -fd`? Możesz stracić wprowadzone zmiany",
|
||||
}, &i18n.Message{
|
||||
ID: "SureTo",
|
||||
Other: "Jesteś pewny, że chcesz {{.deleteVerb}} {{.fileName}} (stracisz swoje wprowadzone zmiany)?",
|
||||
@@ -332,9 +332,6 @@ func addPolish(i18nObject *i18n.Bundle) error {
|
||||
}, &i18n.Message{
|
||||
ID: "CantCloseConfirmationPrompt",
|
||||
Other: "Nie można zamknąć monitu potwierdzenia: {{.error}}",
|
||||
}, &i18n.Message{
|
||||
ID: "ClearFilePanel",
|
||||
Other: "Wyczyść panel plików",
|
||||
}, &i18n.Message{
|
||||
ID: "MergeAborted",
|
||||
Other: "Scalanie anulowane",
|
||||
@@ -371,9 +368,6 @@ func addPolish(i18nObject *i18n.Bundle) error {
|
||||
}, &i18n.Message{
|
||||
ID: "AnonymousReportingPrompt",
|
||||
Other: "Włączyć anonimowe raportowanie błędów w celu pomocy w usprawnianiu lazygita (enter/esc)?",
|
||||
}, &i18n.Message{
|
||||
ID: "removeFile",
|
||||
Other: `usuń jeśli nie śledzony / przełącz jeśli śledzony`,
|
||||
}, &i18n.Message{
|
||||
ID: "editFile",
|
||||
Other: `edytuj plik`,
|
||||
@@ -386,9 +380,6 @@ func addPolish(i18nObject *i18n.Bundle) error {
|
||||
}, &i18n.Message{
|
||||
ID: "refreshFiles",
|
||||
Other: `odśwież pliki`,
|
||||
}, &i18n.Message{
|
||||
ID: "resetHard",
|
||||
Other: `zresetuj twardo i usuń niepotwierdzone pliki`,
|
||||
}, &i18n.Message{
|
||||
ID: "mergeIntoCurrentBranch",
|
||||
Other: `scal do obecnej gałęzi`,
|
||||
@@ -429,7 +420,7 @@ func addPolish(i18nObject *i18n.Bundle) error {
|
||||
ID: "StageLine",
|
||||
Other: `zatwierdź linię`,
|
||||
}, &i18n.Message{
|
||||
ID: "EscapeStaging",
|
||||
ID: "ReturnToFilesPanel",
|
||||
Other: `wróć do panelu plików`,
|
||||
}, &i18n.Message{
|
||||
ID: "CantFindHunks",
|
||||
@@ -466,13 +457,7 @@ func addPolish(i18nObject *i18n.Bundle) error {
|
||||
Other: "Normal",
|
||||
}, &i18n.Message{
|
||||
ID: "softReset",
|
||||
Other: "soft reset to last commit",
|
||||
}, &i18n.Message{
|
||||
ID: "SoftReset",
|
||||
Other: "Soft reset",
|
||||
}, &i18n.Message{
|
||||
ID: "ConfirmSoftReset",
|
||||
Other: "Are you sure you want to `reset --soft HEAD^`? The changes in your topmost commit will be placed in your working tree",
|
||||
Other: "soft reset",
|
||||
}, &i18n.Message{
|
||||
ID: "SureSquashThisCommit",
|
||||
Other: "Are you sure you want to squash this commit into the commit below?",
|
||||
@@ -502,7 +487,7 @@ func addPolish(i18nObject *i18n.Bundle) error {
|
||||
Other: "amend commit with staged changes",
|
||||
}, &i18n.Message{
|
||||
ID: "FoundConflicts",
|
||||
Other: "Damn, conflicts! To abort press 'esc', otherwise press 'enter'",
|
||||
Other: "Conflicts! To abort press 'esc', otherwise press 'enter'",
|
||||
}, &i18n.Message{
|
||||
ID: "FoundConflictsTitle",
|
||||
Other: "Auto-merge failed",
|
||||
@@ -560,6 +545,9 @@ func addPolish(i18nObject *i18n.Bundle) error {
|
||||
}, &i18n.Message{
|
||||
ID: "CannotRebaseOntoFirstCommit",
|
||||
Other: "You cannot interactive rebase onto the first commit",
|
||||
}, &i18n.Message{
|
||||
ID: "CannotSquashOntoSecondCommit",
|
||||
Other: "You cannot squash/fixup onto the second commit",
|
||||
}, &i18n.Message{
|
||||
ID: "Donate",
|
||||
Other: "Donate",
|
||||
@@ -626,6 +614,135 @@ func addPolish(i18nObject *i18n.Bundle) error {
|
||||
}, &i18n.Message{
|
||||
ID: "CherryPickingStatus",
|
||||
Other: "cherry-picking",
|
||||
}, &i18n.Message{
|
||||
ID: "CommitFiles",
|
||||
Other: "Commit files",
|
||||
}, &i18n.Message{
|
||||
ID: "viewCommitFiles",
|
||||
Other: "view commit's files",
|
||||
}, &i18n.Message{
|
||||
ID: "CommitFilesTitle",
|
||||
Other: "Commit files",
|
||||
}, &i18n.Message{
|
||||
ID: "goBack",
|
||||
Other: "go back",
|
||||
}, &i18n.Message{
|
||||
ID: "NoCommiteFiles",
|
||||
Other: "No files for this commit",
|
||||
}, &i18n.Message{
|
||||
ID: "checkoutCommitFile",
|
||||
Other: "checkout file",
|
||||
}, &i18n.Message{
|
||||
ID: "discardOldFileChange",
|
||||
Other: "discard this commit's changes to this file",
|
||||
}, &i18n.Message{
|
||||
ID: "DiscardFileChangesTitle",
|
||||
Other: "Discard file changes",
|
||||
}, &i18n.Message{
|
||||
ID: "DiscardFileChangesPrompt",
|
||||
Other: "Are you sure you want to discard this commit's changes to this file? If this file was created in this commit, it will be deleted",
|
||||
}, &i18n.Message{
|
||||
ID: "DisabledForGPG",
|
||||
Other: "Feature not available for users using GPG",
|
||||
}, &i18n.Message{
|
||||
ID: "CreateRepo",
|
||||
Other: "Not in a git repository. Create a new git repository? (y/n): ",
|
||||
}, &i18n.Message{
|
||||
ID: "AutoStashTitle",
|
||||
Other: "Autostash?",
|
||||
}, &i18n.Message{
|
||||
ID: "AutoStashPrompt",
|
||||
Other: "You must stash and pop your changes to bring them across. Do this automatically? (enter/esc)",
|
||||
}, &i18n.Message{
|
||||
ID: "StashPrefix",
|
||||
Other: "Auto-stashing changes for ",
|
||||
}, &i18n.Message{
|
||||
ID: "viewDiscardOptions",
|
||||
Other: "view 'discard changes' options",
|
||||
}, &i18n.Message{
|
||||
ID: "cancel",
|
||||
Other: "cancel",
|
||||
}, &i18n.Message{
|
||||
ID: "discardAllChanges",
|
||||
Other: "discard all changes",
|
||||
}, &i18n.Message{
|
||||
ID: "discardUnstagedChanges",
|
||||
Other: "discard unstaged changes",
|
||||
}, &i18n.Message{
|
||||
ID: "discardAllChangesToAllFiles",
|
||||
Other: "nuke working tree",
|
||||
}, &i18n.Message{
|
||||
ID: "discardAnyUnstagedChanges",
|
||||
Other: "discard unstaged changes",
|
||||
}, &i18n.Message{
|
||||
ID: "discardUntrackedFiles",
|
||||
Other: "discard untracked files",
|
||||
}, &i18n.Message{
|
||||
ID: "hardReset",
|
||||
Other: "hard reset",
|
||||
}, &i18n.Message{
|
||||
ID: "viewResetOptions",
|
||||
Other: `view reset options`,
|
||||
}, &i18n.Message{
|
||||
ID: "createFixupCommit",
|
||||
Other: `create fixup commit for this commit`,
|
||||
}, &i18n.Message{
|
||||
ID: "squashAboveCommits",
|
||||
Other: `squash above commits`,
|
||||
}, &i18n.Message{
|
||||
ID: "SquashAboveCommits",
|
||||
Other: `Squash above commits`,
|
||||
}, &i18n.Message{
|
||||
ID: "SureSquashAboveCommits",
|
||||
Other: `Are you sure you want to squash all fixup! commits above {{.commit}}?`,
|
||||
}, &i18n.Message{
|
||||
ID: "CreateFixupCommit",
|
||||
Other: `Create fixup commit`,
|
||||
}, &i18n.Message{
|
||||
ID: "SureCreateFixupCommit",
|
||||
Other: `Are you sure you want to create a fixup! commit for commit {{.commit}}?`,
|
||||
}, &i18n.Message{
|
||||
ID: "executeCustomCommand",
|
||||
Other: "execute custom command",
|
||||
}, &i18n.Message{
|
||||
ID: "CustomCommand",
|
||||
Other: "Custom Command:",
|
||||
}, &i18n.Message{
|
||||
ID: "commitChangesWithoutHook",
|
||||
Other: "commit changes without pre-commit hook",
|
||||
}, &i18n.Message{
|
||||
ID: "SkipHookPrefixNotConfigured",
|
||||
Other: "You have not configured a commit message prefix for skipping hooks. Set `git.skipHookPrefix = 'WIP'` in your config",
|
||||
}, &i18n.Message{
|
||||
ID: "resetTo",
|
||||
Other: `reset to`,
|
||||
}, &i18n.Message{
|
||||
ID: "pressEnterToReturn",
|
||||
Other: "Press enter to return to lazygit",
|
||||
}, &i18n.Message{
|
||||
ID: "viewStashOptions",
|
||||
Other: "view stash options",
|
||||
}, &i18n.Message{
|
||||
ID: "stashAllChanges",
|
||||
Other: "przechowaj pliki",
|
||||
}, &i18n.Message{
|
||||
ID: "stashStagedChanges",
|
||||
Other: "stash staged changes",
|
||||
}, &i18n.Message{
|
||||
ID: "stashOptions",
|
||||
Other: "Stash options",
|
||||
}, &i18n.Message{
|
||||
ID: "notARepository",
|
||||
Other: "Error: must be run inside a git repository",
|
||||
}, &i18n.Message{
|
||||
ID: "jump",
|
||||
Other: "jump to panel",
|
||||
}, &i18n.Message{
|
||||
ID: "ExitLineByLineMode",
|
||||
Other: `exit line-by-line mode`,
|
||||
}, &i18n.Message{
|
||||
ID: "EnterUpstream",
|
||||
Other: `Enter upstream as '<remote> <branchname>'`,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultTextColor is the default text color
|
||||
DefaultTextColor = color.FgWhite
|
||||
// DefaultHiTextColor is the default highlighted text color
|
||||
DefaultHiTextColor = color.FgHiWhite
|
||||
|
||||
// GocuiDefaultTextColor does the same as DefaultTextColor but this one only colors gocui default text colors
|
||||
GocuiDefaultTextColor gocui.Attribute
|
||||
|
||||
// ActiveBorderColor is the border color of the active frame
|
||||
ActiveBorderColor gocui.Attribute
|
||||
|
||||
// InactiveBorderColor is the border color of the inactive active frames
|
||||
InactiveBorderColor gocui.Attribute
|
||||
)
|
||||
|
||||
// UpdateTheme updates all theme variables
|
||||
func UpdateTheme(userConfig *viper.Viper) {
|
||||
ActiveBorderColor = getColor(userConfig.GetStringSlice("gui.theme.activeBorderColor"))
|
||||
InactiveBorderColor = getColor(userConfig.GetStringSlice("gui.theme.inactiveBorderColor"))
|
||||
|
||||
isLightTheme := userConfig.GetBool("gui.theme.lightTheme")
|
||||
if isLightTheme {
|
||||
DefaultTextColor = color.FgBlack
|
||||
DefaultHiTextColor = color.FgHiBlack
|
||||
GocuiDefaultTextColor = gocui.ColorBlack
|
||||
} else {
|
||||
DefaultTextColor = color.FgWhite
|
||||
DefaultHiTextColor = color.FgHiWhite
|
||||
GocuiDefaultTextColor = gocui.ColorWhite
|
||||
}
|
||||
}
|
||||
|
||||
// getAttribute gets the gocui color attribute from the string
|
||||
func getAttribute(key string) gocui.Attribute {
|
||||
colorMap := map[string]gocui.Attribute{
|
||||
"default": gocui.ColorDefault,
|
||||
"black": gocui.ColorBlack,
|
||||
"red": gocui.ColorRed,
|
||||
"green": gocui.ColorGreen,
|
||||
"yellow": gocui.ColorYellow,
|
||||
"blue": gocui.ColorBlue,
|
||||
"magenta": gocui.ColorMagenta,
|
||||
"cyan": gocui.ColorCyan,
|
||||
"white": gocui.ColorWhite,
|
||||
"bold": gocui.AttrBold,
|
||||
"reverse": gocui.AttrReverse,
|
||||
"underline": gocui.AttrUnderline,
|
||||
}
|
||||
value, present := colorMap[key]
|
||||
if present {
|
||||
return value
|
||||
}
|
||||
return gocui.ColorWhite
|
||||
}
|
||||
|
||||
// getColor bitwise OR's a list of attributes obtained via the given keys
|
||||
func getColor(keys []string) gocui.Attribute {
|
||||
var attribute gocui.Attribute
|
||||
for _, key := range keys {
|
||||
attribute |= getAttribute(key)
|
||||
}
|
||||
return attribute
|
||||
}
|
||||
|
||||
// GetAttribute gets the gocui color attribute from the string
|
||||
func GetAttribute(key string) gocui.Attribute {
|
||||
colorMap := map[string]gocui.Attribute{
|
||||
"default": gocui.ColorDefault,
|
||||
"black": gocui.ColorBlack,
|
||||
"red": gocui.ColorRed,
|
||||
"green": gocui.ColorGreen,
|
||||
"yellow": gocui.ColorYellow,
|
||||
"blue": gocui.ColorBlue,
|
||||
"magenta": gocui.ColorMagenta,
|
||||
"cyan": gocui.ColorCyan,
|
||||
"white": gocui.ColorWhite,
|
||||
"bold": gocui.AttrBold,
|
||||
"reverse": gocui.AttrReverse,
|
||||
"underline": gocui.AttrUnderline,
|
||||
}
|
||||
value, present := colorMap[key]
|
||||
if present {
|
||||
return value
|
||||
}
|
||||
return gocui.ColorWhite
|
||||
}
|
||||
|
||||
// GetColor bitwise OR's a list of attributes obtained via the given keys
|
||||
func GetColor(keys []string) gocui.Attribute {
|
||||
var attribute gocui.Attribute
|
||||
for _, key := range keys {
|
||||
attribute |= GetAttribute(key)
|
||||
}
|
||||
return attribute
|
||||
}
|
||||
+21
-15
@@ -3,9 +3,8 @@ package updates
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
@@ -16,7 +15,6 @@ import (
|
||||
|
||||
"github.com/kardianos/osext"
|
||||
|
||||
getter "github.com/jesseduffield/go-getter"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
@@ -255,21 +253,34 @@ func (u *Updater) update(newVersion string) error {
|
||||
}
|
||||
|
||||
func (u *Updater) downloadAndInstall(rawUrl string) error {
|
||||
url, err := url.Parse(rawUrl)
|
||||
configDir := u.Config.GetUserConfigDir()
|
||||
u.Log.Info("Download directory is " + configDir)
|
||||
|
||||
tempPath := filepath.Join(configDir, "temp_lazygit")
|
||||
u.Log.Info("Temp path to binary is " + tempPath)
|
||||
|
||||
// Create the file
|
||||
out, err := os.Create(tempPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
g := new(getter.HttpGetter)
|
||||
tempDir, err := ioutil.TempDir("", "lazygit")
|
||||
// Get the data
|
||||
resp, err := http.Get(rawUrl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
u.Log.Info("Temp directory is " + tempDir)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Get it!
|
||||
if err := g.Get(tempDir, url); err != nil {
|
||||
// Check server response
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("error while trying to download latest lazygit: %s", resp.Status)
|
||||
}
|
||||
|
||||
// Write the body to file
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -280,12 +291,7 @@ func (u *Updater) downloadAndInstall(rawUrl string) error {
|
||||
}
|
||||
u.Log.Info("Binary path is " + binaryPath)
|
||||
|
||||
binaryName := filepath.Base(binaryPath)
|
||||
u.Log.Info("Binary name is " + binaryName)
|
||||
|
||||
// Verify the main file exists
|
||||
tempPath := filepath.Join(tempDir, binaryName)
|
||||
u.Log.Info("Temp path to binary is " + tempPath)
|
||||
if _, err := os.Stat(tempPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+54
-5
@@ -33,10 +33,11 @@ func SplitLines(multilineString string) []string {
|
||||
|
||||
// WithPadding pads a string as much as you want
|
||||
func WithPadding(str string, padding int) string {
|
||||
if padding-len(str) < 0 {
|
||||
uncoloredStr := Decolorise(str)
|
||||
if padding < len(uncoloredStr) {
|
||||
return str
|
||||
}
|
||||
return str + strings.Repeat(" ", padding-len(str))
|
||||
return str + strings.Repeat(" ", padding-len(uncoloredStr))
|
||||
}
|
||||
|
||||
// ColoredString takes a string and a colour attribute and returns a colored
|
||||
@@ -225,6 +226,16 @@ func IncludesString(list []string, a string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IncludesInt if the list contains the Int
|
||||
func IncludesInt(list []int, a int) bool {
|
||||
for _, b := range list {
|
||||
if b == a {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NextIndex returns the index of the element that comes after the given number
|
||||
func NextIndex(numbers []int, currentNumber int) int {
|
||||
for index, number := range numbers {
|
||||
@@ -232,21 +243,59 @@ func NextIndex(numbers []int, currentNumber int) int {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return 0
|
||||
return len(numbers) - 1
|
||||
}
|
||||
|
||||
// PrevIndex returns the index that comes before the given number, cycling if we reach the end
|
||||
func PrevIndex(numbers []int, currentNumber int) int {
|
||||
end := len(numbers) - 1
|
||||
for i := end; i >= 0; i -= 1 {
|
||||
for i := end; i >= 0; i-- {
|
||||
if numbers[i] < currentNumber {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return end
|
||||
return 0
|
||||
}
|
||||
|
||||
func AsJson(i interface{}) string {
|
||||
bytes, _ := json.MarshalIndent(i, "", " ")
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
// UnionInt returns the union of two int arrays
|
||||
func UnionInt(a, b []int) []int {
|
||||
m := make(map[int]bool)
|
||||
|
||||
for _, item := range a {
|
||||
m[item] = true
|
||||
}
|
||||
|
||||
for _, item := range b {
|
||||
if _, ok := m[item]; !ok {
|
||||
// this does not mutate the original a slice
|
||||
// though it does mutate the backing array I believe
|
||||
// but that doesn't matter because if you later want to append to the
|
||||
// original a it must see that the backing array has been changed
|
||||
// and create a new one
|
||||
a = append(a, item)
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// DifferenceInt returns the difference of two int arrays
|
||||
func DifferenceInt(a, b []int) []int {
|
||||
result := []int{}
|
||||
m := make(map[int]bool)
|
||||
|
||||
for _, item := range b {
|
||||
m[item] = true
|
||||
}
|
||||
|
||||
for _, item := range a {
|
||||
if _, ok := m[item]; !ok {
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ func TestResolvePlaceholderString(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
assert.EqualValues(t, string(s.expected), ResolvePlaceholderString(s.templateString, s.arguments))
|
||||
assert.EqualValues(t, s.expected, ResolvePlaceholderString(s.templateString, s.arguments))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -485,7 +485,7 @@ func TestNextIndex(t *testing.T) {
|
||||
"no elements",
|
||||
[]int{},
|
||||
1,
|
||||
0,
|
||||
-1,
|
||||
},
|
||||
{
|
||||
"one element",
|
||||
@@ -503,7 +503,7 @@ func TestNextIndex(t *testing.T) {
|
||||
"two elements, giving second one",
|
||||
[]int{1, 2},
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
},
|
||||
{
|
||||
"three elements, giving second one",
|
||||
@@ -534,7 +534,7 @@ func TestPrevIndex(t *testing.T) {
|
||||
"no elements",
|
||||
[]int{},
|
||||
1,
|
||||
-1,
|
||||
0,
|
||||
},
|
||||
{
|
||||
"one element",
|
||||
@@ -546,7 +546,7 @@ func TestPrevIndex(t *testing.T) {
|
||||
"two elements",
|
||||
[]int{1, 2},
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
},
|
||||
{
|
||||
"three elements, giving second one",
|
||||
|
||||
@@ -25,16 +25,21 @@ type bindingSection struct {
|
||||
}
|
||||
|
||||
func main() {
|
||||
langs := []string{"pl", "nl", "en"}
|
||||
mConfig, _ := config.NewAppConfig("", "", "", "", "", true)
|
||||
mApp, _ := app.NewApp(mConfig)
|
||||
lang := mApp.Tr.GetLanguage()
|
||||
file, _ := os.Create("Keybindings_" + lang + ".md")
|
||||
|
||||
bindingSections := getBindingSections(mApp)
|
||||
for _, lang := range langs {
|
||||
os.Setenv("LC_ALL", lang)
|
||||
mApp, _ := app.NewApp(mConfig)
|
||||
file, err := os.Create(getProjectRoot() + "/docs/keybindings/Keybindings_" + lang + ".md")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
content := formatSections(mApp, bindingSections)
|
||||
|
||||
writeString(file, content)
|
||||
bindingSections := getBindingSections(mApp)
|
||||
content := formatSections(mApp, bindingSections)
|
||||
writeString(file, content)
|
||||
}
|
||||
}
|
||||
|
||||
func writeString(file *os.File, str string) {
|
||||
@@ -54,6 +59,9 @@ func formatTitle(title string) string {
|
||||
}
|
||||
|
||||
func formatBinding(binding *gui.Binding) string {
|
||||
if binding.Alternative != "" {
|
||||
return fmt.Sprintf(" <kbd>%s</kbd>: %s (%s)\n", binding.GetKey(), binding.Description, binding.Alternative)
|
||||
}
|
||||
return fmt.Sprintf(" <kbd>%s</kbd>: %s\n", binding.GetKey(), binding.Description)
|
||||
}
|
||||
|
||||
@@ -75,15 +83,13 @@ func getBindingSections(mApp *app.App) []*bindingSection {
|
||||
bindingSections = addBinding(title, bindingSections, binding)
|
||||
}
|
||||
|
||||
for view, contexts := range mApp.Gui.GetContextMap() {
|
||||
for contextName, contextBindings := range contexts {
|
||||
translatedView := localisedTitle(mApp, view)
|
||||
translatedContextName := localisedTitle(mApp, contextName)
|
||||
title := fmt.Sprintf("%s (%s)", translatedView, translatedContextName)
|
||||
for contextName, contextBindings := range mApp.Gui.GetContextMap() {
|
||||
translatedView := localisedTitle(mApp, contextBindings[0].ViewName)
|
||||
translatedContextName := localisedTitle(mApp, contextName)
|
||||
title := fmt.Sprintf("%s (%s)", translatedView, translatedContextName)
|
||||
|
||||
for _, binding := range contextBindings {
|
||||
bindingSections = addBinding(title, bindingSections, binding)
|
||||
}
|
||||
for _, binding := range contextBindings {
|
||||
bindingSections = addBinding(title, bindingSections, binding)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +97,7 @@ func getBindingSections(mApp *app.App) []*bindingSection {
|
||||
}
|
||||
|
||||
func addBinding(title string, bindingSections []*bindingSection, binding *gui.Binding) []*bindingSection {
|
||||
if binding.Description == "" {
|
||||
if binding.Description == "" && binding.Alternative == "" {
|
||||
return bindingSections
|
||||
}
|
||||
|
||||
@@ -124,3 +130,11 @@ func formatSections(mApp *app.App, bindingSections []*bindingSection) string {
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
func getProjectRoot() string {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return strings.Split(dir, "lazygit")[0] + "lazygit"
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ func main() {
|
||||
|
||||
splitVersion := strings.Split(stringVersion, ".")
|
||||
patch := splitVersion[len(splitVersion)-1]
|
||||
newPatch, err := strconv.Atoi(patch)
|
||||
newPatch, _ := strconv.Atoi(patch)
|
||||
splitVersion[len(splitVersion)-1] = strconv.FormatInt(int64(newPatch)+1, 10)
|
||||
newVersion := strings.Join(splitVersion, ".")
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
set -e
|
||||
echo "" > coverage.txt
|
||||
|
||||
export GOFLAGS=-mod=vendor
|
||||
|
||||
use_go_test=false
|
||||
if command -v gotest; then
|
||||
use_go_test=true
|
||||
@@ -10,7 +12,7 @@ fi
|
||||
|
||||
for d in $( find ./* -maxdepth 10 ! -path "./vendor*" ! -path "./.git*" ! -path "./scripts*" -type d); do
|
||||
if ls $d/*.go &> /dev/null; then
|
||||
args="-v -race -coverprofile=profile.out -covermode=atomic $d"
|
||||
args="-race -coverprofile=profile.out -covermode=atomic $d"
|
||||
if [ "$use_go_test" == true ]; then
|
||||
gotest $args
|
||||
else
|
||||
|
||||
@@ -133,4 +133,23 @@ echo "once upon a time there was a horse" >> file5
|
||||
git add file5
|
||||
git commit -m "fourth commit on master"
|
||||
|
||||
# git merge develop # should have a merge conflict here
|
||||
|
||||
# this is for the autostash feature
|
||||
|
||||
git checkout -b base_branch
|
||||
|
||||
echo "original1\noriginal2\noriginal3" > file
|
||||
git add file
|
||||
git commit -m "file"
|
||||
|
||||
git checkout -b other_branch
|
||||
|
||||
git checkout base_branch
|
||||
|
||||
echo "new1\noriginal2\noriginal3" > file
|
||||
git add file
|
||||
git commit -m "file changed"
|
||||
|
||||
git checkout other_branch
|
||||
|
||||
echo "new2\noriginal2\noriginal3" > file
|
||||
|
||||
-202
@@ -1,202 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
AWS SDK for Go
|
||||
Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
Copyright 2014-2015 Stripe, Inc.
|
||||
-145
@@ -1,145 +0,0 @@
|
||||
// Package awserr represents API error interface accessors for the SDK.
|
||||
package awserr
|
||||
|
||||
// An Error wraps lower level errors with code, message and an original error.
|
||||
// The underlying concrete error type may also satisfy other interfaces which
|
||||
// can be to used to obtain more specific information about the error.
|
||||
//
|
||||
// Calling Error() or String() will always include the full information about
|
||||
// an error based on its underlying type.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// output, err := s3manage.Upload(svc, input, opts)
|
||||
// if err != nil {
|
||||
// if awsErr, ok := err.(awserr.Error); ok {
|
||||
// // Get error details
|
||||
// log.Println("Error:", awsErr.Code(), awsErr.Message())
|
||||
//
|
||||
// // Prints out full error message, including original error if there was one.
|
||||
// log.Println("Error:", awsErr.Error())
|
||||
//
|
||||
// // Get original error
|
||||
// if origErr := awsErr.OrigErr(); origErr != nil {
|
||||
// // operate on original error.
|
||||
// }
|
||||
// } else {
|
||||
// fmt.Println(err.Error())
|
||||
// }
|
||||
// }
|
||||
//
|
||||
type Error interface {
|
||||
// Satisfy the generic error interface.
|
||||
error
|
||||
|
||||
// Returns the short phrase depicting the classification of the error.
|
||||
Code() string
|
||||
|
||||
// Returns the error details message.
|
||||
Message() string
|
||||
|
||||
// Returns the original error if one was set. Nil is returned if not set.
|
||||
OrigErr() error
|
||||
}
|
||||
|
||||
// BatchError is a batch of errors which also wraps lower level errors with
|
||||
// code, message, and original errors. Calling Error() will include all errors
|
||||
// that occurred in the batch.
|
||||
//
|
||||
// Deprecated: Replaced with BatchedErrors. Only defined for backwards
|
||||
// compatibility.
|
||||
type BatchError interface {
|
||||
// Satisfy the generic error interface.
|
||||
error
|
||||
|
||||
// Returns the short phrase depicting the classification of the error.
|
||||
Code() string
|
||||
|
||||
// Returns the error details message.
|
||||
Message() string
|
||||
|
||||
// Returns the original error if one was set. Nil is returned if not set.
|
||||
OrigErrs() []error
|
||||
}
|
||||
|
||||
// BatchedErrors is a batch of errors which also wraps lower level errors with
|
||||
// code, message, and original errors. Calling Error() will include all errors
|
||||
// that occurred in the batch.
|
||||
//
|
||||
// Replaces BatchError
|
||||
type BatchedErrors interface {
|
||||
// Satisfy the base Error interface.
|
||||
Error
|
||||
|
||||
// Returns the original error if one was set. Nil is returned if not set.
|
||||
OrigErrs() []error
|
||||
}
|
||||
|
||||
// New returns an Error object described by the code, message, and origErr.
|
||||
//
|
||||
// If origErr satisfies the Error interface it will not be wrapped within a new
|
||||
// Error object and will instead be returned.
|
||||
func New(code, message string, origErr error) Error {
|
||||
var errs []error
|
||||
if origErr != nil {
|
||||
errs = append(errs, origErr)
|
||||
}
|
||||
return newBaseError(code, message, errs)
|
||||
}
|
||||
|
||||
// NewBatchError returns an BatchedErrors with a collection of errors as an
|
||||
// array of errors.
|
||||
func NewBatchError(code, message string, errs []error) BatchedErrors {
|
||||
return newBaseError(code, message, errs)
|
||||
}
|
||||
|
||||
// A RequestFailure is an interface to extract request failure information from
|
||||
// an Error such as the request ID of the failed request returned by a service.
|
||||
// RequestFailures may not always have a requestID value if the request failed
|
||||
// prior to reaching the service such as a connection error.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// output, err := s3manage.Upload(svc, input, opts)
|
||||
// if err != nil {
|
||||
// if reqerr, ok := err.(RequestFailure); ok {
|
||||
// log.Println("Request failed", reqerr.Code(), reqerr.Message(), reqerr.RequestID())
|
||||
// } else {
|
||||
// log.Println("Error:", err.Error())
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Combined with awserr.Error:
|
||||
//
|
||||
// output, err := s3manage.Upload(svc, input, opts)
|
||||
// if err != nil {
|
||||
// if awsErr, ok := err.(awserr.Error); ok {
|
||||
// // Generic AWS Error with Code, Message, and original error (if any)
|
||||
// fmt.Println(awsErr.Code(), awsErr.Message(), awsErr.OrigErr())
|
||||
//
|
||||
// if reqErr, ok := err.(awserr.RequestFailure); ok {
|
||||
// // A service error occurred
|
||||
// fmt.Println(reqErr.StatusCode(), reqErr.RequestID())
|
||||
// }
|
||||
// } else {
|
||||
// fmt.Println(err.Error())
|
||||
// }
|
||||
// }
|
||||
//
|
||||
type RequestFailure interface {
|
||||
Error
|
||||
|
||||
// The status code of the HTTP response.
|
||||
StatusCode() int
|
||||
|
||||
// The request ID returned by the service for a request failure. This will
|
||||
// be empty if no request ID is available such as the request failed due
|
||||
// to a connection error.
|
||||
RequestID() string
|
||||
}
|
||||
|
||||
// NewRequestFailure returns a new request error wrapper for the given Error
|
||||
// provided.
|
||||
func NewRequestFailure(err Error, statusCode int, reqID string) RequestFailure {
|
||||
return newRequestError(err, statusCode, reqID)
|
||||
}
|
||||
-194
@@ -1,194 +0,0 @@
|
||||
package awserr
|
||||
|
||||
import "fmt"
|
||||
|
||||
// SprintError returns a string of the formatted error code.
|
||||
//
|
||||
// Both extra and origErr are optional. If they are included their lines
|
||||
// will be added, but if they are not included their lines will be ignored.
|
||||
func SprintError(code, message, extra string, origErr error) string {
|
||||
msg := fmt.Sprintf("%s: %s", code, message)
|
||||
if extra != "" {
|
||||
msg = fmt.Sprintf("%s\n\t%s", msg, extra)
|
||||
}
|
||||
if origErr != nil {
|
||||
msg = fmt.Sprintf("%s\ncaused by: %s", msg, origErr.Error())
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
// A baseError wraps the code and message which defines an error. It also
|
||||
// can be used to wrap an original error object.
|
||||
//
|
||||
// Should be used as the root for errors satisfying the awserr.Error. Also
|
||||
// for any error which does not fit into a specific error wrapper type.
|
||||
type baseError struct {
|
||||
// Classification of error
|
||||
code string
|
||||
|
||||
// Detailed information about error
|
||||
message string
|
||||
|
||||
// Optional original error this error is based off of. Allows building
|
||||
// chained errors.
|
||||
errs []error
|
||||
}
|
||||
|
||||
// newBaseError returns an error object for the code, message, and errors.
|
||||
//
|
||||
// code is a short no whitespace phrase depicting the classification of
|
||||
// the error that is being created.
|
||||
//
|
||||
// message is the free flow string containing detailed information about the
|
||||
// error.
|
||||
//
|
||||
// origErrs is the error objects which will be nested under the new errors to
|
||||
// be returned.
|
||||
func newBaseError(code, message string, origErrs []error) *baseError {
|
||||
b := &baseError{
|
||||
code: code,
|
||||
message: message,
|
||||
errs: origErrs,
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// Error returns the string representation of the error.
|
||||
//
|
||||
// See ErrorWithExtra for formatting.
|
||||
//
|
||||
// Satisfies the error interface.
|
||||
func (b baseError) Error() string {
|
||||
size := len(b.errs)
|
||||
if size > 0 {
|
||||
return SprintError(b.code, b.message, "", errorList(b.errs))
|
||||
}
|
||||
|
||||
return SprintError(b.code, b.message, "", nil)
|
||||
}
|
||||
|
||||
// String returns the string representation of the error.
|
||||
// Alias for Error to satisfy the stringer interface.
|
||||
func (b baseError) String() string {
|
||||
return b.Error()
|
||||
}
|
||||
|
||||
// Code returns the short phrase depicting the classification of the error.
|
||||
func (b baseError) Code() string {
|
||||
return b.code
|
||||
}
|
||||
|
||||
// Message returns the error details message.
|
||||
func (b baseError) Message() string {
|
||||
return b.message
|
||||
}
|
||||
|
||||
// OrigErr returns the original error if one was set. Nil is returned if no
|
||||
// error was set. This only returns the first element in the list. If the full
|
||||
// list is needed, use BatchedErrors.
|
||||
func (b baseError) OrigErr() error {
|
||||
switch len(b.errs) {
|
||||
case 0:
|
||||
return nil
|
||||
case 1:
|
||||
return b.errs[0]
|
||||
default:
|
||||
if err, ok := b.errs[0].(Error); ok {
|
||||
return NewBatchError(err.Code(), err.Message(), b.errs[1:])
|
||||
}
|
||||
return NewBatchError("BatchedErrors",
|
||||
"multiple errors occurred", b.errs)
|
||||
}
|
||||
}
|
||||
|
||||
// OrigErrs returns the original errors if one was set. An empty slice is
|
||||
// returned if no error was set.
|
||||
func (b baseError) OrigErrs() []error {
|
||||
return b.errs
|
||||
}
|
||||
|
||||
// So that the Error interface type can be included as an anonymous field
|
||||
// in the requestError struct and not conflict with the error.Error() method.
|
||||
type awsError Error
|
||||
|
||||
// A requestError wraps a request or service error.
|
||||
//
|
||||
// Composed of baseError for code, message, and original error.
|
||||
type requestError struct {
|
||||
awsError
|
||||
statusCode int
|
||||
requestID string
|
||||
}
|
||||
|
||||
// newRequestError returns a wrapped error with additional information for
|
||||
// request status code, and service requestID.
|
||||
//
|
||||
// Should be used to wrap all request which involve service requests. Even if
|
||||
// the request failed without a service response, but had an HTTP status code
|
||||
// that may be meaningful.
|
||||
//
|
||||
// Also wraps original errors via the baseError.
|
||||
func newRequestError(err Error, statusCode int, requestID string) *requestError {
|
||||
return &requestError{
|
||||
awsError: err,
|
||||
statusCode: statusCode,
|
||||
requestID: requestID,
|
||||
}
|
||||
}
|
||||
|
||||
// Error returns the string representation of the error.
|
||||
// Satisfies the error interface.
|
||||
func (r requestError) Error() string {
|
||||
extra := fmt.Sprintf("status code: %d, request id: %s",
|
||||
r.statusCode, r.requestID)
|
||||
return SprintError(r.Code(), r.Message(), extra, r.OrigErr())
|
||||
}
|
||||
|
||||
// String returns the string representation of the error.
|
||||
// Alias for Error to satisfy the stringer interface.
|
||||
func (r requestError) String() string {
|
||||
return r.Error()
|
||||
}
|
||||
|
||||
// StatusCode returns the wrapped status code for the error
|
||||
func (r requestError) StatusCode() int {
|
||||
return r.statusCode
|
||||
}
|
||||
|
||||
// RequestID returns the wrapped requestID
|
||||
func (r requestError) RequestID() string {
|
||||
return r.requestID
|
||||
}
|
||||
|
||||
// OrigErrs returns the original errors if one was set. An empty slice is
|
||||
// returned if no error was set.
|
||||
func (r requestError) OrigErrs() []error {
|
||||
if b, ok := r.awsError.(BatchedErrors); ok {
|
||||
return b.OrigErrs()
|
||||
}
|
||||
return []error{r.OrigErr()}
|
||||
}
|
||||
|
||||
// An error list that satisfies the golang interface
|
||||
type errorList []error
|
||||
|
||||
// Error returns the string representation of the error.
|
||||
//
|
||||
// Satisfies the error interface.
|
||||
func (e errorList) Error() string {
|
||||
msg := ""
|
||||
// How do we want to handle the array size being zero
|
||||
if size := len(e); size > 0 {
|
||||
for i := 0; i < size; i++ {
|
||||
msg += fmt.Sprintf("%s", e[i].Error())
|
||||
// We check the next index to see if it is within the slice.
|
||||
// If it is, then we append a newline. We do this, because unit tests
|
||||
// could be broken with the additional '\n'
|
||||
if i+1 < size {
|
||||
msg += "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
return msg
|
||||
}
|
||||
-108
@@ -1,108 +0,0 @@
|
||||
package awsutil
|
||||
|
||||
import (
|
||||
"io"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Copy deeply copies a src structure to dst. Useful for copying request and
|
||||
// response structures.
|
||||
//
|
||||
// Can copy between structs of different type, but will only copy fields which
|
||||
// are assignable, and exist in both structs. Fields which are not assignable,
|
||||
// or do not exist in both structs are ignored.
|
||||
func Copy(dst, src interface{}) {
|
||||
dstval := reflect.ValueOf(dst)
|
||||
if !dstval.IsValid() {
|
||||
panic("Copy dst cannot be nil")
|
||||
}
|
||||
|
||||
rcopy(dstval, reflect.ValueOf(src), true)
|
||||
}
|
||||
|
||||
// CopyOf returns a copy of src while also allocating the memory for dst.
|
||||
// src must be a pointer type or this operation will fail.
|
||||
func CopyOf(src interface{}) (dst interface{}) {
|
||||
dsti := reflect.New(reflect.TypeOf(src).Elem())
|
||||
dst = dsti.Interface()
|
||||
rcopy(dsti, reflect.ValueOf(src), true)
|
||||
return
|
||||
}
|
||||
|
||||
// rcopy performs a recursive copy of values from the source to destination.
|
||||
//
|
||||
// root is used to skip certain aspects of the copy which are not valid
|
||||
// for the root node of a object.
|
||||
func rcopy(dst, src reflect.Value, root bool) {
|
||||
if !src.IsValid() {
|
||||
return
|
||||
}
|
||||
|
||||
switch src.Kind() {
|
||||
case reflect.Ptr:
|
||||
if _, ok := src.Interface().(io.Reader); ok {
|
||||
if dst.Kind() == reflect.Ptr && dst.Elem().CanSet() {
|
||||
dst.Elem().Set(src)
|
||||
} else if dst.CanSet() {
|
||||
dst.Set(src)
|
||||
}
|
||||
} else {
|
||||
e := src.Type().Elem()
|
||||
if dst.CanSet() && !src.IsNil() {
|
||||
if _, ok := src.Interface().(*time.Time); !ok {
|
||||
dst.Set(reflect.New(e))
|
||||
} else {
|
||||
tempValue := reflect.New(e)
|
||||
tempValue.Elem().Set(src.Elem())
|
||||
// Sets time.Time's unexported values
|
||||
dst.Set(tempValue)
|
||||
}
|
||||
}
|
||||
if src.Elem().IsValid() {
|
||||
// Keep the current root state since the depth hasn't changed
|
||||
rcopy(dst.Elem(), src.Elem(), root)
|
||||
}
|
||||
}
|
||||
case reflect.Struct:
|
||||
t := dst.Type()
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
name := t.Field(i).Name
|
||||
srcVal := src.FieldByName(name)
|
||||
dstVal := dst.FieldByName(name)
|
||||
if srcVal.IsValid() && dstVal.CanSet() {
|
||||
rcopy(dstVal, srcVal, false)
|
||||
}
|
||||
}
|
||||
case reflect.Slice:
|
||||
if src.IsNil() {
|
||||
break
|
||||
}
|
||||
|
||||
s := reflect.MakeSlice(src.Type(), src.Len(), src.Cap())
|
||||
dst.Set(s)
|
||||
for i := 0; i < src.Len(); i++ {
|
||||
rcopy(dst.Index(i), src.Index(i), false)
|
||||
}
|
||||
case reflect.Map:
|
||||
if src.IsNil() {
|
||||
break
|
||||
}
|
||||
|
||||
s := reflect.MakeMap(src.Type())
|
||||
dst.Set(s)
|
||||
for _, k := range src.MapKeys() {
|
||||
v := src.MapIndex(k)
|
||||
v2 := reflect.New(v.Type()).Elem()
|
||||
rcopy(v2, v, false)
|
||||
dst.SetMapIndex(k, v2)
|
||||
}
|
||||
default:
|
||||
// Assign the value if possible. If its not assignable, the value would
|
||||
// need to be converted and the impact of that may be unexpected, or is
|
||||
// not compatible with the dst type.
|
||||
if src.Type().AssignableTo(dst.Type()) {
|
||||
dst.Set(src)
|
||||
}
|
||||
}
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
package awsutil
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// DeepEqual returns if the two values are deeply equal like reflect.DeepEqual.
|
||||
// In addition to this, this method will also dereference the input values if
|
||||
// possible so the DeepEqual performed will not fail if one parameter is a
|
||||
// pointer and the other is not.
|
||||
//
|
||||
// DeepEqual will not perform indirection of nested values of the input parameters.
|
||||
func DeepEqual(a, b interface{}) bool {
|
||||
ra := reflect.Indirect(reflect.ValueOf(a))
|
||||
rb := reflect.Indirect(reflect.ValueOf(b))
|
||||
|
||||
if raValid, rbValid := ra.IsValid(), rb.IsValid(); !raValid && !rbValid {
|
||||
// If the elements are both nil, and of the same type the are equal
|
||||
// If they are of different types they are not equal
|
||||
return reflect.TypeOf(a) == reflect.TypeOf(b)
|
||||
} else if raValid != rbValid {
|
||||
// Both values must be valid to be equal
|
||||
return false
|
||||
}
|
||||
|
||||
return reflect.DeepEqual(ra.Interface(), rb.Interface())
|
||||
}
|
||||
-222
@@ -1,222 +0,0 @@
|
||||
package awsutil
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jmespath/go-jmespath"
|
||||
)
|
||||
|
||||
var indexRe = regexp.MustCompile(`(.+)\[(-?\d+)?\]$`)
|
||||
|
||||
// rValuesAtPath returns a slice of values found in value v. The values
|
||||
// in v are explored recursively so all nested values are collected.
|
||||
func rValuesAtPath(v interface{}, path string, createPath, caseSensitive, nilTerm bool) []reflect.Value {
|
||||
pathparts := strings.Split(path, "||")
|
||||
if len(pathparts) > 1 {
|
||||
for _, pathpart := range pathparts {
|
||||
vals := rValuesAtPath(v, pathpart, createPath, caseSensitive, nilTerm)
|
||||
if len(vals) > 0 {
|
||||
return vals
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
values := []reflect.Value{reflect.Indirect(reflect.ValueOf(v))}
|
||||
components := strings.Split(path, ".")
|
||||
for len(values) > 0 && len(components) > 0 {
|
||||
var index *int64
|
||||
var indexStar bool
|
||||
c := strings.TrimSpace(components[0])
|
||||
if c == "" { // no actual component, illegal syntax
|
||||
return nil
|
||||
} else if caseSensitive && c != "*" && strings.ToLower(c[0:1]) == c[0:1] {
|
||||
// TODO normalize case for user
|
||||
return nil // don't support unexported fields
|
||||
}
|
||||
|
||||
// parse this component
|
||||
if m := indexRe.FindStringSubmatch(c); m != nil {
|
||||
c = m[1]
|
||||
if m[2] == "" {
|
||||
index = nil
|
||||
indexStar = true
|
||||
} else {
|
||||
i, _ := strconv.ParseInt(m[2], 10, 32)
|
||||
index = &i
|
||||
indexStar = false
|
||||
}
|
||||
}
|
||||
|
||||
nextvals := []reflect.Value{}
|
||||
for _, value := range values {
|
||||
// pull component name out of struct member
|
||||
if value.Kind() != reflect.Struct {
|
||||
continue
|
||||
}
|
||||
|
||||
if c == "*" { // pull all members
|
||||
for i := 0; i < value.NumField(); i++ {
|
||||
if f := reflect.Indirect(value.Field(i)); f.IsValid() {
|
||||
nextvals = append(nextvals, f)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
value = value.FieldByNameFunc(func(name string) bool {
|
||||
if c == name {
|
||||
return true
|
||||
} else if !caseSensitive && strings.ToLower(name) == strings.ToLower(c) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
if nilTerm && value.Kind() == reflect.Ptr && len(components[1:]) == 0 {
|
||||
if !value.IsNil() {
|
||||
value.Set(reflect.Zero(value.Type()))
|
||||
}
|
||||
return []reflect.Value{value}
|
||||
}
|
||||
|
||||
if createPath && value.Kind() == reflect.Ptr && value.IsNil() {
|
||||
// TODO if the value is the terminus it should not be created
|
||||
// if the value to be set to its position is nil.
|
||||
value.Set(reflect.New(value.Type().Elem()))
|
||||
value = value.Elem()
|
||||
} else {
|
||||
value = reflect.Indirect(value)
|
||||
}
|
||||
|
||||
if value.Kind() == reflect.Slice || value.Kind() == reflect.Map {
|
||||
if !createPath && value.IsNil() {
|
||||
value = reflect.ValueOf(nil)
|
||||
}
|
||||
}
|
||||
|
||||
if value.IsValid() {
|
||||
nextvals = append(nextvals, value)
|
||||
}
|
||||
}
|
||||
values = nextvals
|
||||
|
||||
if indexStar || index != nil {
|
||||
nextvals = []reflect.Value{}
|
||||
for _, valItem := range values {
|
||||
value := reflect.Indirect(valItem)
|
||||
if value.Kind() != reflect.Slice {
|
||||
continue
|
||||
}
|
||||
|
||||
if indexStar { // grab all indices
|
||||
for i := 0; i < value.Len(); i++ {
|
||||
idx := reflect.Indirect(value.Index(i))
|
||||
if idx.IsValid() {
|
||||
nextvals = append(nextvals, idx)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// pull out index
|
||||
i := int(*index)
|
||||
if i >= value.Len() { // check out of bounds
|
||||
if createPath {
|
||||
// TODO resize slice
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
} else if i < 0 { // support negative indexing
|
||||
i = value.Len() + i
|
||||
}
|
||||
value = reflect.Indirect(value.Index(i))
|
||||
|
||||
if value.Kind() == reflect.Slice || value.Kind() == reflect.Map {
|
||||
if !createPath && value.IsNil() {
|
||||
value = reflect.ValueOf(nil)
|
||||
}
|
||||
}
|
||||
|
||||
if value.IsValid() {
|
||||
nextvals = append(nextvals, value)
|
||||
}
|
||||
}
|
||||
values = nextvals
|
||||
}
|
||||
|
||||
components = components[1:]
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
// ValuesAtPath returns a list of values at the case insensitive lexical
|
||||
// path inside of a structure.
|
||||
func ValuesAtPath(i interface{}, path string) ([]interface{}, error) {
|
||||
result, err := jmespath.Search(path, i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(result)
|
||||
if !v.IsValid() || (v.Kind() == reflect.Ptr && v.IsNil()) {
|
||||
return nil, nil
|
||||
}
|
||||
if s, ok := result.([]interface{}); ok {
|
||||
return s, err
|
||||
}
|
||||
if v.Kind() == reflect.Map && v.Len() == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if v.Kind() == reflect.Slice {
|
||||
out := make([]interface{}, v.Len())
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
out[i] = v.Index(i).Interface()
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
return []interface{}{result}, nil
|
||||
}
|
||||
|
||||
// SetValueAtPath sets a value at the case insensitive lexical path inside
|
||||
// of a structure.
|
||||
func SetValueAtPath(i interface{}, path string, v interface{}) {
|
||||
if rvals := rValuesAtPath(i, path, true, false, v == nil); rvals != nil {
|
||||
for _, rval := range rvals {
|
||||
if rval.Kind() == reflect.Ptr && rval.IsNil() {
|
||||
continue
|
||||
}
|
||||
setValue(rval, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setValue(dstVal reflect.Value, src interface{}) {
|
||||
if dstVal.Kind() == reflect.Ptr {
|
||||
dstVal = reflect.Indirect(dstVal)
|
||||
}
|
||||
srcVal := reflect.ValueOf(src)
|
||||
|
||||
if !srcVal.IsValid() { // src is literal nil
|
||||
if dstVal.CanAddr() {
|
||||
// Convert to pointer so that pointer's value can be nil'ed
|
||||
// dstVal = dstVal.Addr()
|
||||
}
|
||||
dstVal.Set(reflect.Zero(dstVal.Type()))
|
||||
|
||||
} else if srcVal.Kind() == reflect.Ptr {
|
||||
if srcVal.IsNil() {
|
||||
srcVal = reflect.Zero(dstVal.Type())
|
||||
} else {
|
||||
srcVal = reflect.ValueOf(src).Elem()
|
||||
}
|
||||
dstVal.Set(srcVal)
|
||||
} else {
|
||||
dstVal.Set(srcVal)
|
||||
}
|
||||
|
||||
}
|
||||
-113
@@ -1,113 +0,0 @@
|
||||
package awsutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Prettify returns the string representation of a value.
|
||||
func Prettify(i interface{}) string {
|
||||
var buf bytes.Buffer
|
||||
prettify(reflect.ValueOf(i), 0, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// prettify will recursively walk value v to build a textual
|
||||
// representation of the value.
|
||||
func prettify(v reflect.Value, indent int, buf *bytes.Buffer) {
|
||||
for v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.Struct:
|
||||
strtype := v.Type().String()
|
||||
if strtype == "time.Time" {
|
||||
fmt.Fprintf(buf, "%s", v.Interface())
|
||||
break
|
||||
} else if strings.HasPrefix(strtype, "io.") {
|
||||
buf.WriteString("<buffer>")
|
||||
break
|
||||
}
|
||||
|
||||
buf.WriteString("{\n")
|
||||
|
||||
names := []string{}
|
||||
for i := 0; i < v.Type().NumField(); i++ {
|
||||
name := v.Type().Field(i).Name
|
||||
f := v.Field(i)
|
||||
if name[0:1] == strings.ToLower(name[0:1]) {
|
||||
continue // ignore unexported fields
|
||||
}
|
||||
if (f.Kind() == reflect.Ptr || f.Kind() == reflect.Slice || f.Kind() == reflect.Map) && f.IsNil() {
|
||||
continue // ignore unset fields
|
||||
}
|
||||
names = append(names, name)
|
||||
}
|
||||
|
||||
for i, n := range names {
|
||||
val := v.FieldByName(n)
|
||||
buf.WriteString(strings.Repeat(" ", indent+2))
|
||||
buf.WriteString(n + ": ")
|
||||
prettify(val, indent+2, buf)
|
||||
|
||||
if i < len(names)-1 {
|
||||
buf.WriteString(",\n")
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteString("\n" + strings.Repeat(" ", indent) + "}")
|
||||
case reflect.Slice:
|
||||
strtype := v.Type().String()
|
||||
if strtype == "[]uint8" {
|
||||
fmt.Fprintf(buf, "<binary> len %d", v.Len())
|
||||
break
|
||||
}
|
||||
|
||||
nl, id, id2 := "", "", ""
|
||||
if v.Len() > 3 {
|
||||
nl, id, id2 = "\n", strings.Repeat(" ", indent), strings.Repeat(" ", indent+2)
|
||||
}
|
||||
buf.WriteString("[" + nl)
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
buf.WriteString(id2)
|
||||
prettify(v.Index(i), indent+2, buf)
|
||||
|
||||
if i < v.Len()-1 {
|
||||
buf.WriteString("," + nl)
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteString(nl + id + "]")
|
||||
case reflect.Map:
|
||||
buf.WriteString("{\n")
|
||||
|
||||
for i, k := range v.MapKeys() {
|
||||
buf.WriteString(strings.Repeat(" ", indent+2))
|
||||
buf.WriteString(k.String() + ": ")
|
||||
prettify(v.MapIndex(k), indent+2, buf)
|
||||
|
||||
if i < v.Len()-1 {
|
||||
buf.WriteString(",\n")
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteString("\n" + strings.Repeat(" ", indent) + "}")
|
||||
default:
|
||||
if !v.IsValid() {
|
||||
fmt.Fprint(buf, "<invalid value>")
|
||||
return
|
||||
}
|
||||
format := "%v"
|
||||
switch v.Interface().(type) {
|
||||
case string:
|
||||
format = "%q"
|
||||
case io.ReadSeeker, io.Reader:
|
||||
format = "buffer(%p)"
|
||||
}
|
||||
fmt.Fprintf(buf, format, v.Interface())
|
||||
}
|
||||
}
|
||||
-89
@@ -1,89 +0,0 @@
|
||||
package awsutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// StringValue returns the string representation of a value.
|
||||
func StringValue(i interface{}) string {
|
||||
var buf bytes.Buffer
|
||||
stringValue(reflect.ValueOf(i), 0, &buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func stringValue(v reflect.Value, indent int, buf *bytes.Buffer) {
|
||||
for v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.Struct:
|
||||
buf.WriteString("{\n")
|
||||
|
||||
names := []string{}
|
||||
for i := 0; i < v.Type().NumField(); i++ {
|
||||
name := v.Type().Field(i).Name
|
||||
f := v.Field(i)
|
||||
if name[0:1] == strings.ToLower(name[0:1]) {
|
||||
continue // ignore unexported fields
|
||||
}
|
||||
if (f.Kind() == reflect.Ptr || f.Kind() == reflect.Slice) && f.IsNil() {
|
||||
continue // ignore unset fields
|
||||
}
|
||||
names = append(names, name)
|
||||
}
|
||||
|
||||
for i, n := range names {
|
||||
val := v.FieldByName(n)
|
||||
buf.WriteString(strings.Repeat(" ", indent+2))
|
||||
buf.WriteString(n + ": ")
|
||||
stringValue(val, indent+2, buf)
|
||||
|
||||
if i < len(names)-1 {
|
||||
buf.WriteString(",\n")
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteString("\n" + strings.Repeat(" ", indent) + "}")
|
||||
case reflect.Slice:
|
||||
nl, id, id2 := "", "", ""
|
||||
if v.Len() > 3 {
|
||||
nl, id, id2 = "\n", strings.Repeat(" ", indent), strings.Repeat(" ", indent+2)
|
||||
}
|
||||
buf.WriteString("[" + nl)
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
buf.WriteString(id2)
|
||||
stringValue(v.Index(i), indent+2, buf)
|
||||
|
||||
if i < v.Len()-1 {
|
||||
buf.WriteString("," + nl)
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteString(nl + id + "]")
|
||||
case reflect.Map:
|
||||
buf.WriteString("{\n")
|
||||
|
||||
for i, k := range v.MapKeys() {
|
||||
buf.WriteString(strings.Repeat(" ", indent+2))
|
||||
buf.WriteString(k.String() + ": ")
|
||||
stringValue(v.MapIndex(k), indent+2, buf)
|
||||
|
||||
if i < v.Len()-1 {
|
||||
buf.WriteString(",\n")
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteString("\n" + strings.Repeat(" ", indent) + "}")
|
||||
default:
|
||||
format := "%v"
|
||||
switch v.Interface().(type) {
|
||||
case string:
|
||||
format = "%q"
|
||||
}
|
||||
fmt.Fprintf(buf, format, v.Interface())
|
||||
}
|
||||
}
|
||||
-96
@@ -1,96 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/client/metadata"
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
)
|
||||
|
||||
// A Config provides configuration to a service client instance.
|
||||
type Config struct {
|
||||
Config *aws.Config
|
||||
Handlers request.Handlers
|
||||
Endpoint string
|
||||
SigningRegion string
|
||||
SigningName string
|
||||
|
||||
// States that the signing name did not come from a modeled source but
|
||||
// was derived based on other data. Used by service client constructors
|
||||
// to determine if the signin name can be overriden based on metadata the
|
||||
// service has.
|
||||
SigningNameDerived bool
|
||||
}
|
||||
|
||||
// ConfigProvider provides a generic way for a service client to receive
|
||||
// the ClientConfig without circular dependencies.
|
||||
type ConfigProvider interface {
|
||||
ClientConfig(serviceName string, cfgs ...*aws.Config) Config
|
||||
}
|
||||
|
||||
// ConfigNoResolveEndpointProvider same as ConfigProvider except it will not
|
||||
// resolve the endpoint automatically. The service client's endpoint must be
|
||||
// provided via the aws.Config.Endpoint field.
|
||||
type ConfigNoResolveEndpointProvider interface {
|
||||
ClientConfigNoResolveEndpoint(cfgs ...*aws.Config) Config
|
||||
}
|
||||
|
||||
// A Client implements the base client request and response handling
|
||||
// used by all service clients.
|
||||
type Client struct {
|
||||
request.Retryer
|
||||
metadata.ClientInfo
|
||||
|
||||
Config aws.Config
|
||||
Handlers request.Handlers
|
||||
}
|
||||
|
||||
// New will return a pointer to a new initialized service client.
|
||||
func New(cfg aws.Config, info metadata.ClientInfo, handlers request.Handlers, options ...func(*Client)) *Client {
|
||||
svc := &Client{
|
||||
Config: cfg,
|
||||
ClientInfo: info,
|
||||
Handlers: handlers.Copy(),
|
||||
}
|
||||
|
||||
switch retryer, ok := cfg.Retryer.(request.Retryer); {
|
||||
case ok:
|
||||
svc.Retryer = retryer
|
||||
case cfg.Retryer != nil && cfg.Logger != nil:
|
||||
s := fmt.Sprintf("WARNING: %T does not implement request.Retryer; using DefaultRetryer instead", cfg.Retryer)
|
||||
cfg.Logger.Log(s)
|
||||
fallthrough
|
||||
default:
|
||||
maxRetries := aws.IntValue(cfg.MaxRetries)
|
||||
if cfg.MaxRetries == nil || maxRetries == aws.UseServiceDefaultRetries {
|
||||
maxRetries = 3
|
||||
}
|
||||
svc.Retryer = DefaultRetryer{NumMaxRetries: maxRetries}
|
||||
}
|
||||
|
||||
svc.AddDebugHandlers()
|
||||
|
||||
for _, option := range options {
|
||||
option(svc)
|
||||
}
|
||||
|
||||
return svc
|
||||
}
|
||||
|
||||
// NewRequest returns a new Request pointer for the service API
|
||||
// operation and parameters.
|
||||
func (c *Client) NewRequest(operation *request.Operation, params interface{}, data interface{}) *request.Request {
|
||||
return request.New(c.Config, c.ClientInfo, c.Handlers, c.Retryer, operation, params, data)
|
||||
}
|
||||
|
||||
// AddDebugHandlers injects debug logging handlers into the service to log request
|
||||
// debug information.
|
||||
func (c *Client) AddDebugHandlers() {
|
||||
if !c.Config.LogLevel.AtLeast(aws.LogDebug) {
|
||||
return
|
||||
}
|
||||
|
||||
c.Handlers.Send.PushFrontNamed(LogHTTPRequestHandler)
|
||||
c.Handlers.Send.PushBackNamed(LogHTTPResponseHandler)
|
||||
}
|
||||
-116
@@ -1,116 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
"github.com/aws/aws-sdk-go/internal/sdkrand"
|
||||
)
|
||||
|
||||
// DefaultRetryer implements basic retry logic using exponential backoff for
|
||||
// most services. If you want to implement custom retry logic, implement the
|
||||
// request.Retryer interface or create a structure type that composes this
|
||||
// struct and override the specific methods. For example, to override only
|
||||
// the MaxRetries method:
|
||||
//
|
||||
// type retryer struct {
|
||||
// client.DefaultRetryer
|
||||
// }
|
||||
//
|
||||
// // This implementation always has 100 max retries
|
||||
// func (d retryer) MaxRetries() int { return 100 }
|
||||
type DefaultRetryer struct {
|
||||
NumMaxRetries int
|
||||
}
|
||||
|
||||
// MaxRetries returns the number of maximum returns the service will use to make
|
||||
// an individual API request.
|
||||
func (d DefaultRetryer) MaxRetries() int {
|
||||
return d.NumMaxRetries
|
||||
}
|
||||
|
||||
// RetryRules returns the delay duration before retrying this request again
|
||||
func (d DefaultRetryer) RetryRules(r *request.Request) time.Duration {
|
||||
// Set the upper limit of delay in retrying at ~five minutes
|
||||
minTime := 30
|
||||
throttle := d.shouldThrottle(r)
|
||||
if throttle {
|
||||
if delay, ok := getRetryDelay(r); ok {
|
||||
return delay
|
||||
}
|
||||
|
||||
minTime = 500
|
||||
}
|
||||
|
||||
retryCount := r.RetryCount
|
||||
if throttle && retryCount > 8 {
|
||||
retryCount = 8
|
||||
} else if retryCount > 13 {
|
||||
retryCount = 13
|
||||
}
|
||||
|
||||
delay := (1 << uint(retryCount)) * (sdkrand.SeededRand.Intn(minTime) + minTime)
|
||||
return time.Duration(delay) * time.Millisecond
|
||||
}
|
||||
|
||||
// ShouldRetry returns true if the request should be retried.
|
||||
func (d DefaultRetryer) ShouldRetry(r *request.Request) bool {
|
||||
// If one of the other handlers already set the retry state
|
||||
// we don't want to override it based on the service's state
|
||||
if r.Retryable != nil {
|
||||
return *r.Retryable
|
||||
}
|
||||
|
||||
if r.HTTPResponse.StatusCode >= 500 && r.HTTPResponse.StatusCode != 501 {
|
||||
return true
|
||||
}
|
||||
return r.IsErrorRetryable() || d.shouldThrottle(r)
|
||||
}
|
||||
|
||||
// ShouldThrottle returns true if the request should be throttled.
|
||||
func (d DefaultRetryer) shouldThrottle(r *request.Request) bool {
|
||||
switch r.HTTPResponse.StatusCode {
|
||||
case 429:
|
||||
case 502:
|
||||
case 503:
|
||||
case 504:
|
||||
default:
|
||||
return r.IsErrorThrottle()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// This will look in the Retry-After header, RFC 7231, for how long
|
||||
// it will wait before attempting another request
|
||||
func getRetryDelay(r *request.Request) (time.Duration, bool) {
|
||||
if !canUseRetryAfterHeader(r) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
delayStr := r.HTTPResponse.Header.Get("Retry-After")
|
||||
if len(delayStr) == 0 {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
delay, err := strconv.Atoi(delayStr)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return time.Duration(delay) * time.Second, true
|
||||
}
|
||||
|
||||
// Will look at the status code to see if the retry header pertains to
|
||||
// the status code.
|
||||
func canUseRetryAfterHeader(r *request.Request) bool {
|
||||
switch r.HTTPResponse.StatusCode {
|
||||
case 429:
|
||||
case 503:
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
-184
@@ -1,184 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http/httputil"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
)
|
||||
|
||||
const logReqMsg = `DEBUG: Request %s/%s Details:
|
||||
---[ REQUEST POST-SIGN ]-----------------------------
|
||||
%s
|
||||
-----------------------------------------------------`
|
||||
|
||||
const logReqErrMsg = `DEBUG ERROR: Request %s/%s:
|
||||
---[ REQUEST DUMP ERROR ]-----------------------------
|
||||
%s
|
||||
------------------------------------------------------`
|
||||
|
||||
type logWriter struct {
|
||||
// Logger is what we will use to log the payload of a response.
|
||||
Logger aws.Logger
|
||||
// buf stores the contents of what has been read
|
||||
buf *bytes.Buffer
|
||||
}
|
||||
|
||||
func (logger *logWriter) Write(b []byte) (int, error) {
|
||||
return logger.buf.Write(b)
|
||||
}
|
||||
|
||||
type teeReaderCloser struct {
|
||||
// io.Reader will be a tee reader that is used during logging.
|
||||
// This structure will read from a body and write the contents to a logger.
|
||||
io.Reader
|
||||
// Source is used just to close when we are done reading.
|
||||
Source io.ReadCloser
|
||||
}
|
||||
|
||||
func (reader *teeReaderCloser) Close() error {
|
||||
return reader.Source.Close()
|
||||
}
|
||||
|
||||
// LogHTTPRequestHandler is a SDK request handler to log the HTTP request sent
|
||||
// to a service. Will include the HTTP request body if the LogLevel of the
|
||||
// request matches LogDebugWithHTTPBody.
|
||||
var LogHTTPRequestHandler = request.NamedHandler{
|
||||
Name: "awssdk.client.LogRequest",
|
||||
Fn: logRequest,
|
||||
}
|
||||
|
||||
func logRequest(r *request.Request) {
|
||||
logBody := r.Config.LogLevel.Matches(aws.LogDebugWithHTTPBody)
|
||||
bodySeekable := aws.IsReaderSeekable(r.Body)
|
||||
|
||||
b, err := httputil.DumpRequestOut(r.HTTPRequest, logBody)
|
||||
if err != nil {
|
||||
r.Config.Logger.Log(fmt.Sprintf(logReqErrMsg,
|
||||
r.ClientInfo.ServiceName, r.Operation.Name, err))
|
||||
return
|
||||
}
|
||||
|
||||
if logBody {
|
||||
if !bodySeekable {
|
||||
r.SetReaderBody(aws.ReadSeekCloser(r.HTTPRequest.Body))
|
||||
}
|
||||
// Reset the request body because dumpRequest will re-wrap the r.HTTPRequest's
|
||||
// Body as a NoOpCloser and will not be reset after read by the HTTP
|
||||
// client reader.
|
||||
r.ResetBody()
|
||||
}
|
||||
|
||||
r.Config.Logger.Log(fmt.Sprintf(logReqMsg,
|
||||
r.ClientInfo.ServiceName, r.Operation.Name, string(b)))
|
||||
}
|
||||
|
||||
// LogHTTPRequestHeaderHandler is a SDK request handler to log the HTTP request sent
|
||||
// to a service. Will only log the HTTP request's headers. The request payload
|
||||
// will not be read.
|
||||
var LogHTTPRequestHeaderHandler = request.NamedHandler{
|
||||
Name: "awssdk.client.LogRequestHeader",
|
||||
Fn: logRequestHeader,
|
||||
}
|
||||
|
||||
func logRequestHeader(r *request.Request) {
|
||||
b, err := httputil.DumpRequestOut(r.HTTPRequest, false)
|
||||
if err != nil {
|
||||
r.Config.Logger.Log(fmt.Sprintf(logReqErrMsg,
|
||||
r.ClientInfo.ServiceName, r.Operation.Name, err))
|
||||
return
|
||||
}
|
||||
|
||||
r.Config.Logger.Log(fmt.Sprintf(logReqMsg,
|
||||
r.ClientInfo.ServiceName, r.Operation.Name, string(b)))
|
||||
}
|
||||
|
||||
const logRespMsg = `DEBUG: Response %s/%s Details:
|
||||
---[ RESPONSE ]--------------------------------------
|
||||
%s
|
||||
-----------------------------------------------------`
|
||||
|
||||
const logRespErrMsg = `DEBUG ERROR: Response %s/%s:
|
||||
---[ RESPONSE DUMP ERROR ]-----------------------------
|
||||
%s
|
||||
-----------------------------------------------------`
|
||||
|
||||
// LogHTTPResponseHandler is a SDK request handler to log the HTTP response
|
||||
// received from a service. Will include the HTTP response body if the LogLevel
|
||||
// of the request matches LogDebugWithHTTPBody.
|
||||
var LogHTTPResponseHandler = request.NamedHandler{
|
||||
Name: "awssdk.client.LogResponse",
|
||||
Fn: logResponse,
|
||||
}
|
||||
|
||||
func logResponse(r *request.Request) {
|
||||
lw := &logWriter{r.Config.Logger, bytes.NewBuffer(nil)}
|
||||
|
||||
logBody := r.Config.LogLevel.Matches(aws.LogDebugWithHTTPBody)
|
||||
if logBody {
|
||||
r.HTTPResponse.Body = &teeReaderCloser{
|
||||
Reader: io.TeeReader(r.HTTPResponse.Body, lw),
|
||||
Source: r.HTTPResponse.Body,
|
||||
}
|
||||
}
|
||||
|
||||
handlerFn := func(req *request.Request) {
|
||||
b, err := httputil.DumpResponse(req.HTTPResponse, false)
|
||||
if err != nil {
|
||||
lw.Logger.Log(fmt.Sprintf(logRespErrMsg,
|
||||
req.ClientInfo.ServiceName, req.Operation.Name, err))
|
||||
return
|
||||
}
|
||||
|
||||
lw.Logger.Log(fmt.Sprintf(logRespMsg,
|
||||
req.ClientInfo.ServiceName, req.Operation.Name, string(b)))
|
||||
|
||||
if logBody {
|
||||
b, err := ioutil.ReadAll(lw.buf)
|
||||
if err != nil {
|
||||
lw.Logger.Log(fmt.Sprintf(logRespErrMsg,
|
||||
req.ClientInfo.ServiceName, req.Operation.Name, err))
|
||||
return
|
||||
}
|
||||
|
||||
lw.Logger.Log(string(b))
|
||||
}
|
||||
}
|
||||
|
||||
const handlerName = "awsdk.client.LogResponse.ResponseBody"
|
||||
|
||||
r.Handlers.Unmarshal.SetBackNamed(request.NamedHandler{
|
||||
Name: handlerName, Fn: handlerFn,
|
||||
})
|
||||
r.Handlers.UnmarshalError.SetBackNamed(request.NamedHandler{
|
||||
Name: handlerName, Fn: handlerFn,
|
||||
})
|
||||
}
|
||||
|
||||
// LogHTTPResponseHeaderHandler is a SDK request handler to log the HTTP
|
||||
// response received from a service. Will only log the HTTP response's headers.
|
||||
// The response payload will not be read.
|
||||
var LogHTTPResponseHeaderHandler = request.NamedHandler{
|
||||
Name: "awssdk.client.LogResponseHeader",
|
||||
Fn: logResponseHeader,
|
||||
}
|
||||
|
||||
func logResponseHeader(r *request.Request) {
|
||||
if r.Config.Logger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
b, err := httputil.DumpResponse(r.HTTPResponse, false)
|
||||
if err != nil {
|
||||
r.Config.Logger.Log(fmt.Sprintf(logRespErrMsg,
|
||||
r.ClientInfo.ServiceName, r.Operation.Name, err))
|
||||
return
|
||||
}
|
||||
|
||||
r.Config.Logger.Log(fmt.Sprintf(logRespMsg,
|
||||
r.ClientInfo.ServiceName, r.Operation.Name, string(b)))
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user