1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-03-05 15:15:49 +02:00

Merge branch 'hotfix/cursor-positioning' into feature/recent-repos

This commit is contained in:
Jesse Duffield 2018-09-19 18:40:41 +10:00
commit 768b9453f8
34 changed files with 2434 additions and 445 deletions

View File

@ -2,14 +2,24 @@ version: 2
jobs: jobs:
build: build:
docker: docker:
- image: circleci/golang:1.10 - image: circleci/golang:1.11
working_directory: /go/src/github.com/jesseduffield/lazygit working_directory: /go/src/github.com/jesseduffield/lazygit
steps: steps:
- checkout - checkout
- restore_cache: - run:
keys: name: Ensure go.mod file is up to date
- pkg-cache-{{ checksum "Gopkg.lock" }}-v1 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: - run:
name: Run gofmt -s name: Run gofmt -s
command: | command: |
@ -17,6 +27,9 @@ jobs:
find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \; find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;
exit 1; exit 1;
fi fi
- restore_cache:
keys:
- pkg-cache-{{ checksum "Gopkg.lock" }}-v2
- run: - run:
name: Run tests name: Run tests
command: | command: |
@ -31,7 +44,7 @@ jobs:
command: | command: |
bash <(curl -s https://codecov.io/bash) bash <(curl -s https://codecov.io/bash)
- save_cache: - save_cache:
key: pkg-cache-{{ checksum "Gopkg.lock" }}-v1 key: pkg-cache-{{ checksum "Gopkg.lock" }}-v2
paths: paths:
- ~/.cache/go-build - ~/.cache/go-build

17
.gitignore vendored
View File

@ -3,13 +3,14 @@
# Logs # Logs
*.log *.log
# Extras # Hidden
extra/lgit.rb .*
# TODO
TODO.*
# Notes # Notes
notes/go.notes *.notes
TODO.notes
TODO.md
# Tests # Tests
test/repos/repo test/repos/repo
@ -17,3 +18,9 @@ coverage.txt
# Binaries # Binaries
lazygit lazygit
# Exceptions
!.gitignore
!.goreleaser.yml
!.circleci/
!.github/

62
go.mod Normal file
View File

@ -0,0 +1,62 @@
module github.com/jesseduffield/lazygit
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-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-20180905104005-2cb6e95bbbf8
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/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/nsf/termbox-go v0.0.0-20180613055208-5c94acc5e6eb
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/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/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/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
)

163
go.sum Normal file
View File

@ -0,0 +1,163 @@
github.com/BurntSushi/toml v0.3.0 h1:e1/Ivsx3Z0FVTV0NSOv/aVgbUWyQuzj7DDnFblkRvsY=
github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
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/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/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/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/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
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/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/gliderlabs/ssh v0.1.1 h1:j3L6gSLQalDETeEg/Jg0mGY0/y/N6zI2xX1978P0Uqw=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
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/golang-collections/collections v0.0.0-20130729185459-604e922904d3 h1:zN2lZNZRflqFyxVaTIU61KNKQ9C0055u9CAfpmqUvo4=
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3/go.mod h1:nPpo7qLxd6XL3hWJG/O60sR8ZKfMCiIoNap5GvD12KU=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c h1:16eHWuMGvCjSfgRJKqIzapE78onvvTbdi1rMkU00lZw=
github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
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/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
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-20180905104005-2cb6e95bbbf8 h1:YMYnpIu5HUJfx/yfIwnZhFrgWTg51FQWtvZi+PMzQm8=
github.com/jesseduffield/gocui v0.0.0-20180905104005-2cb6e95bbbf8/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
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/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE=
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
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/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/go.mod h1:e4Di5xjP9oTVrC6y3C7C0HoSYXjSbhh/dU0eUV32nB4=
github.com/nicksnyder/go-i18n/v2 v2.0.0-beta.5 h1:/TjjTS4kg7vC+05gD0LE4+97f/+PRFICnK/7wJPk7kE=
github.com/nicksnyder/go-i18n/v2 v2.0.0-beta.5/go.mod h1:4Opqa6/HIv0lhG3WRAkqzO0afezkRhxXI0P8EJkqeRU=
github.com/nsf/termbox-go v0.0.0-20180613055208-5c94acc5e6eb h1:YahEjAGkJtCrkqgVHhX6n8ZX+CZ3hDRL9fjLYugLfSs=
github.com/nsf/termbox-go v0.0.0-20180613055208-5c94acc5e6eb/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.1 h1:PZSj/UFNaVp3KxrzHOcS7oyuWA7LoOY/77yCTEFu21U=
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/pelletier/go-buffruneio v0.2.0 h1:U4t4R6YkofJ5xHm3dJzuRpPZ0mr5MMCoAWooScCR7aA=
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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/smartystreets/assertions v0.0.0-20180820201707-7c9eb446e3cf h1:6V1qxN6Usn4jy8unvggSJz/NC790tefw8Zdy6OZS5co=
github.com/smartystreets/assertions v0.0.0-20180820201707-7c9eb446e3cf/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a h1:JSvGDIbmil4Ui/dDdFBExb7/cmkNjyX5F97oglmvCDo=
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
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/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/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
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/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/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNjB2u4i700xBkIT4e0=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/ini.v1 v1.38.2 h1:dGcbywv4RufeGeiMycPT/plKB5FtmLKLnWKwBiLhUA4=
gopkg.in/ini.v1 v1.38.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
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-fixtures.v3 v3.1.1 h1:XWW/s5W18RaJpmo1l0IYGqXKuJITWRFuA45iOf1dKJs=
gopkg.in/src-d/go-git-fixtures.v3 v3.1.1/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
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/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.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -3,6 +3,7 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"log"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
@ -40,13 +41,13 @@ func main() {
} }
appConfig, err := config.NewAppConfig("lazygit", version, commit, date, buildSource, debuggingFlag) appConfig, err := config.NewAppConfig("lazygit", version, commit, date, buildSource, debuggingFlag)
if err != nil { if err != nil {
panic(err) log.Fatal(err.Error())
} }
app, err := app.Setup(appConfig) app, err := app.Setup(appConfig)
if err != nil { if err != nil {
app.Log.Error(err.Error()) app.Log.Error(err.Error())
panic(err) log.Fatal(err.Error())
} }
app.Gui.RunWithSubprocesses() app.Gui.RunWithSubprocesses()

View File

@ -14,9 +14,9 @@ type Branch struct {
Recency string Recency string
} }
// GetDisplayString returns the dispaly string of branch // GetDisplayStrings returns the dispaly string of branch
func (b *Branch) GetDisplayString() string { func (b *Branch) GetDisplayStrings() []string {
return utils.WithPadding(b.Recency, 4) + utils.ColoredString(b.Name, b.GetColor()) return []string{b.Recency, utils.ColoredString(b.Name, b.GetColor())}
} }
// GetColor branch color // GetColor branch color

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

@ -0,0 +1,26 @@
package commands
import (
"github.com/fatih/color"
)
// Commit : A git commit
type Commit struct {
Sha string
Name string
Pushed bool
DisplayString string
}
func (c *Commit) GetDisplayStrings() []string {
red := color.New(color.FgRed)
yellow := color.New(color.FgYellow)
white := color.New(color.FgWhite)
shaColor := yellow
if c.Pushed {
shaColor = red
}
return []string{shaColor.Sprint(c.Sha), white.Sprint(c.Name)}
}

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

@ -0,0 +1,36 @@
package commands
import "github.com/fatih/color"
// File : A file from git status
// duplicating this for now
type File struct {
Name string
HasStagedChanges bool
HasUnstagedChanges bool
Tracked bool
Deleted bool
HasMergeConflicts bool
DisplayString string
Type string // one of 'file', 'directory', and 'other'
}
// GetDisplayStrings returns the display string of a file
func (f *File) GetDisplayStrings() []string {
// potentially inefficient to be instantiating these color
// objects with each render
red := color.New(color.FgRed)
green := color.New(color.FgGreen)
if !f.Tracked && !f.HasStagedChanges {
return []string{red.Sprint(f.DisplayString)}
}
output := green.Sprint(f.DisplayString[0:1])
output += red.Sprint(f.DisplayString[1:3])
if f.HasUnstagedChanges {
output += red.Sprint(f.Name)
} else {
output += green.Sprint(f.Name)
}
return []string{output}
}

View File

@ -7,7 +7,6 @@ import (
"os/exec" "os/exec"
"strings" "strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -64,6 +63,8 @@ type GitCommand struct {
Worktree *gogit.Worktree Worktree *gogit.Worktree
Repo *gogit.Repository Repo *gogit.Repository
Tr *i18n.Localizer Tr *i18n.Localizer
getGlobalGitConfig func(string) (string, error)
getLocalGitConfig func(string) (string, error)
} }
// NewGitCommand it runs git commands // NewGitCommand it runs git commands
@ -97,21 +98,23 @@ func NewGitCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localizer)
Tr: tr, Tr: tr,
Worktree: worktree, Worktree: worktree,
Repo: repo, Repo: repo,
getGlobalGitConfig: gitconfig.Global,
getLocalGitConfig: gitconfig.Local,
}, nil }, nil
} }
// GetStashEntries stash entryies // GetStashEntries stash entryies
func (c *GitCommand) GetStashEntries() []StashEntry { func (c *GitCommand) GetStashEntries() []*StashEntry {
rawString, _ := c.OSCommand.RunCommandWithOutput("git stash list --pretty='%gs'") rawString, _ := c.OSCommand.RunCommandWithOutput("git stash list --pretty='%gs'")
stashEntries := []StashEntry{} stashEntries := []*StashEntry{}
for i, line := range utils.SplitLines(rawString) { for i, line := range utils.SplitLines(rawString) {
stashEntries = append(stashEntries, stashEntryFromLine(line, i)) stashEntries = append(stashEntries, stashEntryFromLine(line, i))
} }
return stashEntries return stashEntries
} }
func stashEntryFromLine(line string, index int) StashEntry { func stashEntryFromLine(line string, index int) *StashEntry {
return StashEntry{ return &StashEntry{
Name: line, Name: line,
Index: index, Index: index,
DisplayString: line, DisplayString: line,
@ -123,33 +126,26 @@ func (c *GitCommand) GetStashEntryDiff(index int) (string, error) {
return c.OSCommand.RunCommandWithOutput("git stash show -p --color stash@{" + fmt.Sprint(index) + "}") return c.OSCommand.RunCommandWithOutput("git stash show -p --color stash@{" + fmt.Sprint(index) + "}")
} }
func includes(array []string, str string) bool {
for _, arrayStr := range array {
if arrayStr == str {
return true
}
}
return false
}
// GetStatusFiles git status files // GetStatusFiles git status files
func (c *GitCommand) GetStatusFiles() []File { func (c *GitCommand) GetStatusFiles() []*File {
statusOutput, _ := c.GitStatus() statusOutput, _ := c.GitStatus()
statusStrings := utils.SplitLines(statusOutput) statusStrings := utils.SplitLines(statusOutput)
files := []File{} files := []*File{}
for _, statusString := range statusStrings { for _, statusString := range statusStrings {
change := statusString[0:2] change := statusString[0:2]
stagedChange := change[0:1] stagedChange := change[0:1]
unstagedChange := statusString[1:2] unstagedChange := statusString[1:2]
filename := c.OSCommand.Unquote(statusString[3:]) filename := c.OSCommand.Unquote(statusString[3:])
tracked := !includes([]string{"??", "A ", "AM"}, change) _, untracked := map[string]bool{"??": true, "A ": true, "AM": true}[change]
file := File{ _, hasNoStagedChanges := map[string]bool{" ": true, "U": true, "?": true}[stagedChange]
file := &File{
Name: filename, Name: filename,
DisplayString: statusString, DisplayString: statusString,
HasStagedChanges: !includes([]string{" ", "U", "?"}, stagedChange), HasStagedChanges: !hasNoStagedChanges,
HasUnstagedChanges: unstagedChange != " ", HasUnstagedChanges: unstagedChange != " ",
Tracked: tracked, Tracked: !untracked,
Deleted: unstagedChange == "D" || stagedChange == "D", Deleted: unstagedChange == "D" || stagedChange == "D",
HasMergeConflicts: change == "UU", HasMergeConflicts: change == "UU",
Type: c.OSCommand.FileType(filename), Type: c.OSCommand.FileType(filename),
@ -172,33 +168,42 @@ func (c *GitCommand) StashSave(message string) error {
} }
// MergeStatusFiles merge status files // MergeStatusFiles merge status files
func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []File) []File { func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []*File) []*File {
if len(oldFiles) == 0 { if len(oldFiles) == 0 {
return newFiles return newFiles
} }
headResults := []File{} appendedIndexes := []int{}
tailResults := []File{}
for _, newFile := range newFiles {
var isHeadResult bool
// retain position of files we already could see
result := []*File{}
for _, oldFile := range oldFiles { for _, oldFile := range oldFiles {
for newIndex, newFile := range newFiles {
if oldFile.Name == newFile.Name { if oldFile.Name == newFile.Name {
isHeadResult = true result = append(result, newFile)
appendedIndexes = append(appendedIndexes, newIndex)
break break
} }
} }
if isHeadResult {
headResults = append(headResults, newFile)
continue
} }
tailResults = append(tailResults, newFile) // append any new files to the end
for index, newFile := range newFiles {
if !includesInt(appendedIndexes, index) {
result = append(result, newFile)
}
} }
return append(headResults, tailResults...) return result
}
func includesInt(list []int, a int) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
} }
// GetBranchName branch name // GetBranchName branch name
@ -230,14 +235,14 @@ func (c *GitCommand) UpstreamDifferenceCount() (string, string) {
func (c *GitCommand) GetCommitsToPush() []string { func (c *GitCommand) GetCommitsToPush() []string {
pushables, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..head --abbrev-commit") pushables, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..head --abbrev-commit")
if err != nil { if err != nil {
return make([]string, 0) return []string{}
} }
return utils.SplitLines(pushables) return utils.SplitLines(pushables)
} }
// RenameCommit renames the topmost commit with the given name // RenameCommit renames the topmost commit with the given name
func (c *GitCommand) RenameCommit(name string) error { func (c *GitCommand) RenameCommit(name string) error {
return c.OSCommand.RunCommand("git commit --allow-empty --amend -m " + c.OSCommand.Quote(name)) return c.OSCommand.RunCommand(fmt.Sprintf("git commit --allow-empty --amend -m %s", c.OSCommand.Quote(name)))
} }
// Fetch fetch git repo // Fetch fetch git repo
@ -247,23 +252,23 @@ func (c *GitCommand) Fetch() error {
// ResetToCommit reset to commit // ResetToCommit reset to commit
func (c *GitCommand) ResetToCommit(sha string) error { func (c *GitCommand) ResetToCommit(sha string) error {
return c.OSCommand.RunCommand("git reset " + sha) return c.OSCommand.RunCommand(fmt.Sprintf("git reset %s", sha))
} }
// NewBranch create new branch // NewBranch create new branch
func (c *GitCommand) NewBranch(name string) error { func (c *GitCommand) NewBranch(name string) error {
return c.OSCommand.RunCommand("git checkout -b " + name) return c.OSCommand.RunCommand(fmt.Sprintf("git checkout -b %s", name))
} }
// DeleteBranch delete branch // DeleteBranch delete branch
func (c *GitCommand) DeleteBranch(branch string, force bool) error { func (c *GitCommand) DeleteBranch(branch string, force bool) error {
var command string command := "git branch -d"
if force { if force {
command = "git branch -D" command = "git branch -D"
} else {
command = "git branch -d "
} }
return c.OSCommand.RunCommand(command + branch)
return c.OSCommand.RunCommand(fmt.Sprintf("%s %s", command, branch))
} }
// ListStash list stash // ListStash list stash
@ -273,7 +278,7 @@ func (c *GitCommand) ListStash() (string, error) {
// Merge merge // Merge merge
func (c *GitCommand) Merge(branchName string) error { func (c *GitCommand) Merge(branchName string) error {
return c.OSCommand.RunCommand("git merge --no-edit " + branchName) return c.OSCommand.RunCommand(fmt.Sprintf("git merge --no-edit %s", branchName))
} }
// AbortMerge abort merge // AbortMerge abort merge
@ -281,96 +286,91 @@ func (c *GitCommand) AbortMerge() error {
return c.OSCommand.RunCommand("git merge --abort") return c.OSCommand.RunCommand("git merge --abort")
} }
// UsingGpg tells us whether the user has gpg enabled so that we can know // usingGpg tells us whether the user has gpg enabled so that we can know
// whether we need to run a subprocess to allow them to enter their password // whether we need to run a subprocess to allow them to enter their password
func (c *GitCommand) UsingGpg() bool { func (c *GitCommand) usingGpg() bool {
gpgsign, _ := gitconfig.Global("commit.gpgsign") gpgsign, _ := c.getLocalGitConfig("commit.gpgsign")
if gpgsign == "" { if gpgsign == "" {
gpgsign, _ = gitconfig.Local("commit.gpgsign") gpgsign, _ = c.getGlobalGitConfig("commit.gpgsign")
} }
if gpgsign == "" { value := strings.ToLower(gpgsign)
return false
} return value == "true" || value == "1" || value == "yes" || value == "on"
return true
} }
// Commit commit to git // Commit commits to git
func (c *GitCommand) Commit(g *gocui.Gui, message string) (*exec.Cmd, error) { func (c *GitCommand) Commit(message string) (*exec.Cmd, error) {
command := "git commit -m " + c.OSCommand.Quote(message) command := fmt.Sprintf("git commit -m %s", c.OSCommand.Quote(message))
if c.UsingGpg() { if c.usingGpg() {
return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command), nil return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command), nil
} }
return nil, c.OSCommand.RunCommand(command) return nil, c.OSCommand.RunCommand(command)
} }
// Pull pull from repo // Pull pulls from repo
func (c *GitCommand) Pull() error { func (c *GitCommand) Pull() error {
return c.OSCommand.RunCommand("git pull --no-edit") return c.OSCommand.RunCommand("git pull --no-edit")
} }
// Push push to a branch // Push pushes to a branch
func (c *GitCommand) Push(branchName string, force bool) error { func (c *GitCommand) Push(branchName string, force bool) error {
forceFlag := "" forceFlag := ""
if force { if force {
forceFlag = "--force-with-lease " forceFlag = "--force-with-lease "
} }
return c.OSCommand.RunCommand("git push " + forceFlag + "-u origin " + branchName)
return c.OSCommand.RunCommand(fmt.Sprintf("git push %s -u origin %s", forceFlag, branchName))
} }
// SquashPreviousTwoCommits squashes a commit down to the one below it // SquashPreviousTwoCommits squashes a commit down to the one below it
// retaining the message of the higher commit // retaining the message of the higher commit
func (c *GitCommand) SquashPreviousTwoCommits(message string) error { func (c *GitCommand) SquashPreviousTwoCommits(message string) error {
// TODO: test this // TODO: test this
err := c.OSCommand.RunCommand("git reset --soft HEAD^") if err := c.OSCommand.RunCommand("git reset --soft HEAD^"); err != nil {
if err != nil {
return err return err
} }
// TODO: if password is required, we need to return a subprocess // TODO: if password is required, we need to return a subprocess
return c.OSCommand.RunCommand("git commit --amend -m " + c.OSCommand.Quote(message)) return c.OSCommand.RunCommand(fmt.Sprintf("git commit --amend -m %s", c.OSCommand.Quote(message)))
} }
// SquashFixupCommit squashes a 'FIXUP' commit into the commit beneath it, // SquashFixupCommit squashes a 'FIXUP' commit into the commit beneath it,
// retaining the commit message of the lower commit // retaining the commit message of the lower commit
func (c *GitCommand) SquashFixupCommit(branchName string, shaValue string) error { func (c *GitCommand) SquashFixupCommit(branchName string, shaValue string) error {
var err error
commands := []string{ commands := []string{
"git checkout -q " + shaValue, fmt.Sprintf("git checkout -q %s", shaValue),
"git reset --soft " + shaValue + "^", fmt.Sprintf("git reset --soft %s^", shaValue),
"git commit --amend -C " + shaValue + "^", fmt.Sprintf("git commit --amend -C %s^", shaValue),
"git rebase --onto HEAD " + shaValue + " " + branchName, fmt.Sprintf("git rebase --onto HEAD %s %s", shaValue, branchName),
} }
ret := ""
for _, command := range commands { for _, command := range commands {
c.Log.Info(command) c.Log.Info(command)
output, err := c.OSCommand.RunCommandWithOutput(command)
ret += output if output, err := c.OSCommand.RunCommandWithOutput(command); err != nil {
if err != nil { ret := output
c.Log.Info(ret)
break
}
}
if err != nil {
// We are already in an error state here so we're just going to append // We are already in an error state here so we're just going to append
// the output of these commands // the output of these commands
output, _ := c.OSCommand.RunCommandWithOutput("git branch -d " + shaValue) output, _ := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git branch -d %s", shaValue))
ret += output ret += output
output, _ = c.OSCommand.RunCommandWithOutput("git checkout " + branchName) output, _ = c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git checkout %s", branchName))
ret += output ret += output
}
if err != nil { c.Log.Info(ret)
return errors.New(ret) return errors.New(ret)
} }
}
return nil return nil
} }
// CatFile obtain the contents of a file // CatFile obtains the content of a file
func (c *GitCommand) CatFile(fileName string) (string, error) { func (c *GitCommand) CatFile(fileName string) (string, error) {
return c.OSCommand.RunCommandWithOutput("cat " + c.OSCommand.Quote(fileName)) return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("cat %s", c.OSCommand.Quote(fileName)))
} }
// StageFile stages a file // StageFile stages a file
func (c *GitCommand) StageFile(fileName string) error { func (c *GitCommand) StageFile(fileName string) error {
return c.OSCommand.RunCommand("git add " + c.OSCommand.Quote(fileName)) return c.OSCommand.RunCommand(fmt.Sprintf("git add %s", c.OSCommand.Quote(fileName)))
} }
// StageAll stages all files // StageAll stages all files
@ -385,13 +385,11 @@ func (c *GitCommand) UnstageAll() error {
// UnStageFile unstages a file // UnStageFile unstages a file
func (c *GitCommand) UnStageFile(fileName string, tracked bool) error { func (c *GitCommand) UnStageFile(fileName string, tracked bool) error {
var command string command := "git rm --cached %s"
if tracked { if tracked {
command = "git reset HEAD " command = "git reset HEAD %s"
} else {
command = "git rm --cached "
} }
return c.OSCommand.RunCommand(command + c.OSCommand.Quote(fileName)) return c.OSCommand.RunCommand(fmt.Sprintf(command, c.OSCommand.Quote(fileName)))
} }
// GitStatus returns the plaintext short status of the repo // GitStatus returns the plaintext short status of the repo
@ -409,7 +407,7 @@ func (c *GitCommand) IsInMergeState() (bool, error) {
} }
// RemoveFile directly // RemoveFile directly
func (c *GitCommand) RemoveFile(file File) error { func (c *GitCommand) RemoveFile(file *File) error {
// if the file isn't tracked, we assume you want to delete it // if the file isn't tracked, we assume you want to delete it
if file.HasStagedChanges { if file.HasStagedChanges {
if err := c.OSCommand.RunCommand("git reset -- " + file.Name); err != nil { if err := c.OSCommand.RunCommand("git reset -- " + file.Name); err != nil {
@ -465,17 +463,17 @@ func includesString(list []string, a string) bool {
} }
// GetCommits obtains the commits of the current branch // GetCommits obtains the commits of the current branch
func (c *GitCommand) GetCommits() []Commit { func (c *GitCommand) GetCommits() []*Commit {
pushables := c.GetCommitsToPush() pushables := c.GetCommitsToPush()
log := c.GetLog() log := c.GetLog()
commits := make([]Commit, 0) commits := []*Commit{}
// now we can split it up and turn it into commits // now we can split it up and turn it into commits
lines := utils.SplitLines(log) lines := utils.SplitLines(log)
for _, line := range lines { for _, line := range lines {
splitLine := strings.Split(line, " ") splitLine := strings.Split(line, " ")
sha := splitLine[0] sha := splitLine[0]
pushed := includesString(pushables, sha) pushed := includesString(pushables, sha)
commits = append(commits, Commit{ commits = append(commits, &Commit{
Sha: sha, Sha: sha,
Name: strings.Join(splitLine[1:], " "), Name: strings.Join(splitLine[1:], " "),
Pushed: pushed, Pushed: pushed,
@ -513,7 +511,7 @@ func (c *GitCommand) Show(sha string) string {
} }
// Diff returns the diff of a file // Diff returns the diff of a file
func (c *GitCommand) Diff(file File) string { func (c *GitCommand) Diff(file *File) string {
cachedArg := "" cachedArg := ""
fileName := c.OSCommand.Quote(file.Name) fileName := c.OSCommand.Quote(file.Name)
if file.HasStagedChanges && !file.HasUnstagedChanges { if file.HasStagedChanges && !file.HasUnstagedChanges {

View File

@ -1,32 +1,5 @@
package commands package commands
// File : A staged/unstaged file
type File struct {
Name string
HasStagedChanges bool
HasUnstagedChanges bool
Tracked bool
Deleted bool
HasMergeConflicts bool
DisplayString string
Type string // one of 'file', 'directory', and 'other'
}
// Commit : A git commit
type Commit struct {
Sha string
Name string
Pushed bool
DisplayString string
}
// StashEntry : A git stash entry
type StashEntry struct {
Index int
Name string
DisplayString string
}
// Conflict : A git conflict with a start middle and end corresponding to line // Conflict : A git conflict with a start middle and end corresponding to line
// numbers in the file where the conflict bars appear // numbers in the file where the conflict bars appear
type Conflict struct { type Conflict struct {

File diff suppressed because it is too large Load Diff

View File

@ -22,6 +22,7 @@ type Platform struct {
shellArg string shellArg string
escapedQuote string escapedQuote string
openCommand string openCommand string
fallbackEscapedQuote string
} }
// OSCommand holds all the os commands // OSCommand holds all the os commands
@ -140,7 +141,11 @@ func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *ex
// Quote wraps a message in platform-specific quotation marks // Quote wraps a message in platform-specific quotation marks
func (c *OSCommand) Quote(message string) string { func (c *OSCommand) Quote(message string) string {
message = strings.Replace(message, "`", "\\`", -1) message = strings.Replace(message, "`", "\\`", -1)
return c.Platform.escapedQuote + message + c.Platform.escapedQuote escapedQuote := c.Platform.escapedQuote
if strings.Contains(message, c.Platform.escapedQuote) {
escapedQuote = c.Platform.fallbackEscapedQuote
}
return escapedQuote + message + escapedQuote
} }
// Unquote removes wrapping quotations marks if they are present // Unquote removes wrapping quotations marks if they are present

View File

@ -11,7 +11,8 @@ func getPlatform() *Platform {
os: runtime.GOOS, os: runtime.GOOS,
shell: "bash", shell: "bash",
shellArg: "-c", shellArg: "-c",
escapedQuote: "\"", escapedQuote: "'",
openCommand: "open {{filename}}", openCommand: "open {{filename}}",
fallbackEscapedQuote: "\"",
} }
} }

View File

@ -265,6 +265,32 @@ func TestOSCommandQuote(t *testing.T) {
assert.EqualValues(t, expected, actual) assert.EqualValues(t, expected, actual)
} }
// TestOSCommandQuoteSingleQuote tests the quote function with ' quotes explicitly for Linux
func TestOSCommandQuoteSingleQuote(t *testing.T) {
osCommand := newDummyOSCommand()
osCommand.Platform.os = "linux"
actual := osCommand.Quote("hello 'test'")
expected := osCommand.Platform.fallbackEscapedQuote + "hello 'test'" + osCommand.Platform.fallbackEscapedQuote
assert.EqualValues(t, expected, actual)
}
// TestOSCommandQuoteSingleQuote tests the quote function with " quotes explicitly for Linux
func TestOSCommandQuoteDoubleQuote(t *testing.T) {
osCommand := newDummyOSCommand()
osCommand.Platform.os = "linux"
actual := osCommand.Quote(`hello "test"`)
expected := osCommand.Platform.escapedQuote + "hello \"test\"" + osCommand.Platform.escapedQuote
assert.EqualValues(t, expected, actual)
}
func TestOSCommandUnquote(t *testing.T) { func TestOSCommandUnquote(t *testing.T) {
osCommand := newDummyOSCommand() osCommand := newDummyOSCommand()

View File

@ -6,5 +6,6 @@ func getPlatform() *Platform {
shell: "cmd", shell: "cmd",
shellArg: "/c", shellArg: "/c",
escapedQuote: `\"`, escapedQuote: `\"`,
fallbackEscapedQuote: "\\'",
} }
} }

View File

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

View File

@ -34,7 +34,7 @@ func NewBranchListBuilder(log *logrus.Entry, gitCommand *commands.GitCommand) (*
}, nil }, nil
} }
func (b *BranchListBuilder) obtainCurrentBranch() commands.Branch { func (b *BranchListBuilder) obtainCurrentBranch() *commands.Branch {
// I used go-git for this, but that breaks if you've just done a git init, // I used go-git for this, but that breaks if you've just done a git init,
// even though you're on 'master' // even though you're on 'master'
branchName, err := b.GitCommand.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD") branchName, err := b.GitCommand.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD")
@ -44,11 +44,11 @@ func (b *BranchListBuilder) obtainCurrentBranch() commands.Branch {
panic(err.Error()) panic(err.Error())
} }
} }
return commands.Branch{Name: strings.TrimSpace(branchName), Recency: " *"} return &commands.Branch{Name: strings.TrimSpace(branchName), Recency: " *"}
} }
func (b *BranchListBuilder) obtainReflogBranches() []commands.Branch { func (b *BranchListBuilder) obtainReflogBranches() []*commands.Branch {
branches := make([]commands.Branch, 0) branches := make([]*commands.Branch, 0)
rawString, err := b.GitCommand.OSCommand.RunCommandWithOutput("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD") rawString, err := b.GitCommand.OSCommand.RunCommandWithOutput("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD")
if err != nil { if err != nil {
return branches return branches
@ -58,14 +58,14 @@ func (b *BranchListBuilder) obtainReflogBranches() []commands.Branch {
for _, line := range branchLines { for _, line := range branchLines {
timeNumber, timeUnit, branchName := branchInfoFromLine(line) timeNumber, timeUnit, branchName := branchInfoFromLine(line)
timeUnit = abbreviatedTimeUnit(timeUnit) timeUnit = abbreviatedTimeUnit(timeUnit)
branch := commands.Branch{Name: branchName, Recency: timeNumber + timeUnit} branch := &commands.Branch{Name: branchName, Recency: timeNumber + timeUnit}
branches = append(branches, branch) branches = append(branches, branch)
} }
return branches return branches
} }
func (b *BranchListBuilder) obtainSafeBranches() []commands.Branch { func (b *BranchListBuilder) obtainSafeBranches() []*commands.Branch {
branches := make([]commands.Branch, 0) branches := make([]*commands.Branch, 0)
bIter, err := b.GitCommand.Repo.Branches() bIter, err := b.GitCommand.Repo.Branches()
if err != nil { if err != nil {
@ -73,14 +73,14 @@ func (b *BranchListBuilder) obtainSafeBranches() []commands.Branch {
} }
err = bIter.ForEach(func(b *plumbing.Reference) error { err = bIter.ForEach(func(b *plumbing.Reference) error {
name := b.Name().Short() name := b.Name().Short()
branches = append(branches, commands.Branch{Name: name}) branches = append(branches, &commands.Branch{Name: name})
return nil return nil
}) })
return branches return branches
} }
func (b *BranchListBuilder) appendNewBranches(finalBranches, newBranches, existingBranches []commands.Branch, included bool) []commands.Branch { func (b *BranchListBuilder) appendNewBranches(finalBranches, newBranches, existingBranches []*commands.Branch, included bool) []*commands.Branch {
for _, newBranch := range newBranches { for _, newBranch := range newBranches {
if included == branchIncluded(newBranch.Name, existingBranches) { if included == branchIncluded(newBranch.Name, existingBranches) {
finalBranches = append(finalBranches, newBranch) finalBranches = append(finalBranches, newBranch)
@ -89,7 +89,7 @@ func (b *BranchListBuilder) appendNewBranches(finalBranches, newBranches, existi
return finalBranches return finalBranches
} }
func sanitisedReflogName(reflogBranch commands.Branch, safeBranches []commands.Branch) string { func sanitisedReflogName(reflogBranch *commands.Branch, safeBranches []*commands.Branch) string {
for _, safeBranch := range safeBranches { for _, safeBranch := range safeBranches {
if strings.ToLower(safeBranch.Name) == strings.ToLower(reflogBranch.Name) { if strings.ToLower(safeBranch.Name) == strings.ToLower(reflogBranch.Name) {
return safeBranch.Name return safeBranch.Name
@ -99,15 +99,15 @@ func sanitisedReflogName(reflogBranch commands.Branch, safeBranches []commands.B
} }
// Build the list of branches for the current repo // Build the list of branches for the current repo
func (b *BranchListBuilder) Build() []commands.Branch { func (b *BranchListBuilder) Build() []*commands.Branch {
branches := make([]commands.Branch, 0) branches := make([]*commands.Branch, 0)
head := b.obtainCurrentBranch() head := b.obtainCurrentBranch()
safeBranches := b.obtainSafeBranches() safeBranches := b.obtainSafeBranches()
if len(safeBranches) == 0 { if len(safeBranches) == 0 {
return append(branches, head) return append(branches, head)
} }
reflogBranches := b.obtainReflogBranches() reflogBranches := b.obtainReflogBranches()
reflogBranches = uniqueByName(append([]commands.Branch{head}, reflogBranches...)) reflogBranches = uniqueByName(append([]*commands.Branch{head}, reflogBranches...))
for i, reflogBranch := range reflogBranches { for i, reflogBranch := range reflogBranches {
reflogBranches[i].Name = sanitisedReflogName(reflogBranch, safeBranches) reflogBranches[i].Name = sanitisedReflogName(reflogBranch, safeBranches)
} }
@ -118,7 +118,7 @@ func (b *BranchListBuilder) Build() []commands.Branch {
return branches return branches
} }
func branchIncluded(branchName string, branches []commands.Branch) bool { func branchIncluded(branchName string, branches []*commands.Branch) bool {
for _, existingBranch := range branches { for _, existingBranch := range branches {
if strings.ToLower(existingBranch.Name) == strings.ToLower(branchName) { if strings.ToLower(existingBranch.Name) == strings.ToLower(branchName) {
return true return true
@ -127,8 +127,8 @@ func branchIncluded(branchName string, branches []commands.Branch) bool {
return false return false
} }
func uniqueByName(branches []commands.Branch) []commands.Branch { func uniqueByName(branches []*commands.Branch) []*commands.Branch {
finalBranches := make([]commands.Branch, 0) finalBranches := make([]*commands.Branch, 0)
for _, branch := range branches { for _, branch := range branches {
if branchIncluded(branch.Name, finalBranches) { if branchIncluded(branch.Name, finalBranches) {
continue continue

View File

@ -7,14 +7,15 @@ import (
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/git" "github.com/jesseduffield/lazygit/pkg/git"
"github.com/jesseduffield/lazygit/pkg/utils"
) )
func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error {
index := gui.getItemPosition(v) index := gui.getItemPosition(gui.getBranchesView(g))
if index == 0 { if index == 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("AlreadyCheckedOutBranch")) return gui.createErrorPanel(g, gui.Tr.SLocalize("AlreadyCheckedOutBranch"))
} }
branch := gui.getSelectedBranch(v) branch := gui.getSelectedBranch(gui.getBranchesView(g))
if err := gui.GitCommand.Checkout(branch.Name, false); err != nil { if err := gui.GitCommand.Checkout(branch.Name, false); err != nil {
gui.createErrorPanel(g, err.Error()) gui.createErrorPanel(g, err.Error())
} }
@ -109,22 +110,13 @@ func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error {
return nil return nil
} }
func (gui *Gui) getSelectedBranch(v *gocui.View) commands.Branch { func (gui *Gui) getSelectedBranch(v *gocui.View) *commands.Branch {
lineNumber := gui.getItemPosition(v) lineNumber := gui.getItemPosition(v)
return gui.State.Branches[lineNumber] return gui.State.Branches[lineNumber]
} }
func (gui *Gui) renderBranchesOptions(g *gocui.Gui) error { func (gui *Gui) renderBranchesOptions(g *gocui.Gui) error {
return gui.renderOptionsMap(g, map[string]string{ return gui.renderGlobalOptions(g)
"space": gui.Tr.SLocalize("checkout"),
"f": gui.Tr.SLocalize("forceCheckout"),
"m": gui.Tr.SLocalize("merge"),
"c": gui.Tr.SLocalize("checkoutByName"),
"n": gui.Tr.SLocalize("newBranch"),
"d": gui.Tr.SLocalize("deleteBranch"),
"D": gui.Tr.SLocalize("forceDeleteBranch"),
"← → ↑ ↓": gui.Tr.SLocalize("navigate"),
})
} }
// may want to standardise how these select methods work // may want to standardise how these select methods work
@ -160,10 +152,15 @@ func (gui *Gui) refreshBranches(g *gocui.Gui) error {
return err return err
} }
gui.State.Branches = builder.Build() gui.State.Branches = builder.Build()
v.Clear() v.Clear()
for _, branch := range gui.State.Branches { list, err := utils.RenderList(gui.State.Branches)
fmt.Fprintln(v, branch.GetDisplayString()) if err != nil {
return err
} }
fmt.Fprint(v, list)
gui.resetOrigin(v) gui.resetOrigin(v)
return gui.refreshStatus(g) return gui.refreshStatus(g)
}) })

View File

@ -12,7 +12,7 @@ func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
if message == "" { if message == "" {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CommitWithoutMessageErr")) return gui.createErrorPanel(g, gui.Tr.SLocalize("CommitWithoutMessageErr"))
} }
sub, err := gui.GitCommand.Commit(g, message) sub, err := gui.GitCommand.Commit(message)
if err != nil { if err != nil {
// TODO need to find a way to send through this error // TODO need to find a way to send through this error
if err != gui.Errors.ErrSubProcess { if err != gui.Errors.ErrSubProcess {

View File

@ -2,10 +2,11 @@ package gui
import ( import (
"errors" "errors"
"fmt"
"github.com/fatih/color"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
) )
func (gui *Gui) refreshCommits(g *gocui.Gui) error { func (gui *Gui) refreshCommits(g *gocui.Gui) error {
@ -15,20 +16,14 @@ func (gui *Gui) refreshCommits(g *gocui.Gui) error {
if err != nil { if err != nil {
panic(err) panic(err)
} }
v.Clear() v.Clear()
red := color.New(color.FgRed) list, err := utils.RenderList(gui.State.Commits)
yellow := color.New(color.FgYellow) if err != nil {
white := color.New(color.FgWhite) return err
shaColor := white
for _, commit := range gui.State.Commits {
if commit.Pushed {
shaColor = red
} else {
shaColor = yellow
}
shaColor.Fprint(v, commit.Sha+" ")
white.Fprintln(v, commit.Name)
} }
fmt.Fprint(v, list)
gui.refreshStatus(g) gui.refreshStatus(g)
if g.CurrentView().Name() == "commits" { if g.CurrentView().Name() == "commits" {
gui.handleCommitSelect(g, v) gui.handleCommitSelect(g, v)
@ -59,13 +54,7 @@ func (gui *Gui) handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error
} }
func (gui *Gui) renderCommitsOptions(g *gocui.Gui) error { func (gui *Gui) renderCommitsOptions(g *gocui.Gui) error {
return gui.renderOptionsMap(g, map[string]string{ return gui.renderGlobalOptions(g)
"s": gui.Tr.SLocalize("squashDown"),
"r": gui.Tr.SLocalize("rename"),
"g": gui.Tr.SLocalize("resetToThisCommit"),
"f": gui.Tr.SLocalize("fixupCommit"),
"← → ↑ ↓": gui.Tr.SLocalize("navigate"),
})
} }
func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
@ -105,7 +94,7 @@ func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
} }
// TODO: move to files panel // TODO: move to files panel
func (gui *Gui) anyUnStagedChanges(files []commands.File) bool { func (gui *Gui) anyUnStagedChanges(files []*commands.File) bool {
for _, file := range files { for _, file := range files {
if file.Tracked && file.HasUnstagedChanges { if file.Tracked && file.HasUnstagedChanges {
return true return true
@ -140,10 +129,10 @@ func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
} }
func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
if gui.getItemPosition(v) != 0 { if gui.getItemPosition(gui.getCommitsView(g)) != 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlyRenameTopCommit")) return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlyRenameTopCommit"))
} }
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 { if err := gui.GitCommand.RenameCommit(v.Buffer()); err != nil {
return gui.createErrorPanel(g, err.Error()) return gui.createErrorPanel(g, err.Error())
} }
@ -156,7 +145,7 @@ func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
} }
func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
if gui.getItemPosition(v) != 0 { if gui.getItemPosition(gui.getCommitsView(g)) != 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlyRenameTopCommit")) return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlyRenameTopCommit"))
} }
@ -168,13 +157,13 @@ func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
return nil return nil
} }
func (gui *Gui) getSelectedCommit(g *gocui.Gui) (commands.Commit, error) { func (gui *Gui) getSelectedCommit(g *gocui.Gui) (*commands.Commit, error) {
v, err := g.View("commits") v, err := g.View("commits")
if err != nil { if err != nil {
panic(err) panic(err)
} }
if len(gui.State.Commits) == 0 { if len(gui.State.Commits) == 0 {
return commands.Commit{}, errors.New(gui.Tr.SLocalize("NoCommitsThisBranch")) return &commands.Commit{}, errors.New(gui.Tr.SLocalize("NoCommitsThisBranch"))
} }
lineNumber := gui.getItemPosition(v) lineNumber := gui.getItemPosition(v)
if lineNumber > len(gui.State.Commits)-1 { if lineNumber > len(gui.State.Commits)-1 {

View File

@ -7,16 +7,17 @@ import (
// "strings" // "strings"
"fmt"
"strings" "strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
) )
func (gui *Gui) stagedFiles() []commands.File { func (gui *Gui) stagedFiles() []*commands.File {
files := gui.State.Files files := gui.State.Files
result := make([]commands.File, 0) result := make([]*commands.File, 0)
for _, file := range files { for _, file := range files {
if file.HasStagedChanges { if file.HasStagedChanges {
result = append(result, file) result = append(result, file)
@ -25,9 +26,9 @@ func (gui *Gui) stagedFiles() []commands.File {
return result return result
} }
func (gui *Gui) trackedFiles() []commands.File { func (gui *Gui) trackedFiles() []*commands.File {
files := gui.State.Files files := gui.State.Files
result := make([]commands.File, 0) result := make([]*commands.File, 0)
for _, file := range files { for _, file := range files {
if file.Tracked { if file.Tracked {
result = append(result, file) result = append(result, file)
@ -116,9 +117,9 @@ func (gui *Gui) handleAddPatch(g *gocui.Gui, v *gocui.View) error {
return gui.Errors.ErrSubProcess return gui.Errors.ErrSubProcess
} }
func (gui *Gui) getSelectedFile(g *gocui.Gui) (commands.File, error) { func (gui *Gui) getSelectedFile(g *gocui.Gui) (*commands.File, error) {
if len(gui.State.Files) == 0 { if len(gui.State.Files) == 0 {
return commands.File{}, gui.Errors.ErrNoFiles return &commands.File{}, gui.Errors.ErrNoFiles
} }
filesView, err := g.View("files") filesView, err := g.View("files")
if err != nil { if err != nil {
@ -172,31 +173,7 @@ func (gui *Gui) handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
} }
func (gui *Gui) renderfilesOptions(g *gocui.Gui, file *commands.File) error { func (gui *Gui) renderfilesOptions(g *gocui.Gui, file *commands.File) error {
optionsMap := map[string]string{ return gui.renderGlobalOptions(g)
"← → ↑ ↓": gui.Tr.SLocalize("navigate"),
"S": gui.Tr.SLocalize("stashFiles"),
"c": gui.Tr.SLocalize("CommitChanges"),
"o": gui.Tr.SLocalize("open"),
"i": gui.Tr.SLocalize("ignore"),
"d": gui.Tr.SLocalize("delete"),
"space": gui.Tr.SLocalize("toggleStaged"),
"R": gui.Tr.SLocalize("refresh"),
"t": gui.Tr.SLocalize("addPatch"),
"e": gui.Tr.SLocalize("edit"),
"a": gui.Tr.SLocalize("toggleStagedAll"),
"PgUp/PgDn": gui.Tr.SLocalize("scroll"),
}
if gui.State.HasMergeConflicts {
optionsMap["a"] = gui.Tr.SLocalize("abortMerge")
optionsMap["m"] = gui.Tr.SLocalize("resolveMergeConflicts")
}
if file == nil {
return gui.renderOptionsMap(g, optionsMap)
}
if file.Tracked {
optionsMap["d"] = gui.Tr.SLocalize("checkout")
}
return gui.renderOptionsMap(g, optionsMap)
} }
func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View) error {
@ -208,7 +185,9 @@ func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View) error {
gui.renderString(g, "main", gui.Tr.SLocalize("NoChangedFiles")) gui.renderString(g, "main", gui.Tr.SLocalize("NoChangedFiles"))
return gui.renderfilesOptions(g, nil) return gui.renderfilesOptions(g, nil)
} }
gui.renderfilesOptions(g, &file) if err := gui.renderfilesOptions(g, file); err != nil {
return err
}
var content string var content string
if file.HasMergeConflicts { if file.HasMergeConflicts {
return gui.refreshMergePanel(g) return gui.refreshMergePanel(g)
@ -299,24 +278,6 @@ func (gui *Gui) updateHasMergeConflictStatus() error {
return nil return nil
} }
func (gui *Gui) renderFile(file commands.File, filesView *gocui.View) {
// potentially inefficient to be instantiating these color
// objects with each render
red := color.New(color.FgRed)
green := color.New(color.FgGreen)
if !file.Tracked && !file.HasStagedChanges {
red.Fprintln(filesView, file.DisplayString)
return
}
green.Fprint(filesView, file.DisplayString[0:1])
red.Fprint(filesView, file.DisplayString[1:3])
if file.HasUnstagedChanges {
red.Fprintln(filesView, file.Name)
} else {
green.Fprintln(filesView, file.Name)
}
}
func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) { func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) {
item, err := gui.getSelectedFile(g) item, err := gui.getSelectedFile(g)
if err != nil { if err != nil {
@ -342,10 +303,14 @@ func (gui *Gui) refreshFiles(g *gocui.Gui) error {
return err return err
} }
gui.refreshStateFiles() gui.refreshStateFiles()
filesView.Clear() filesView.Clear()
for _, file := range gui.State.Files { list, err := utils.RenderList(gui.State.Files)
gui.renderFile(file, filesView) if err != nil {
return err
} }
fmt.Fprint(filesView, list)
gui.correctCursor(filesView) gui.correctCursor(filesView)
if filesView == g.CurrentView() { if filesView == g.CurrentView() {
gui.handleFileSelect(g, filesView) gui.handleFileSelect(g, filesView)

View File

@ -73,10 +73,10 @@ type Gui struct {
} }
type guiState struct { type guiState struct {
Files []commands.File Files []*commands.File
Branches []commands.Branch Branches []*commands.Branch
Commits []commands.Commit Commits []*commands.Commit
StashEntries []commands.StashEntry StashEntries []*commands.StashEntry
PreviousView string PreviousView string
HasMergeConflicts bool HasMergeConflicts bool
ConflictIndex int ConflictIndex int
@ -91,10 +91,10 @@ type guiState struct {
func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *commands.OSCommand, tr *i18n.Localizer, config config.AppConfigurer, updater *updates.Updater) (*Gui, error) { func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *commands.OSCommand, tr *i18n.Localizer, config config.AppConfigurer, updater *updates.Updater) (*Gui, error) {
initialState := guiState{ initialState := guiState{
Files: make([]commands.File, 0), Files: make([]*commands.File, 0),
PreviousView: "files", PreviousView: "files",
Commits: make([]commands.Commit, 0), Commits: make([]*commands.Commit, 0),
StashEntries: make([]commands.StashEntry, 0), StashEntries: make([]*commands.StashEntry, 0),
ConflictIndex: 0, ConflictIndex: 0,
ConflictTop: true, ConflictTop: true,
Conflicts: make([]commands.Conflict, 0), Conflicts: make([]commands.Conflict, 0),
@ -387,6 +387,15 @@ func (gui *Gui) renderAppStatus(g *gocui.Gui) error {
return nil return nil
} }
func (gui *Gui) renderGlobalOptions(g *gocui.Gui) error {
return gui.renderOptionsMap(g, map[string]string{
"PgUp/PgDn": gui.Tr.SLocalize("scroll"),
"← → ↑ ↓": gui.Tr.SLocalize("navigate"),
"esc/q": gui.Tr.SLocalize("close"),
"x": gui.Tr.SLocalize("menu"),
})
}
func (gui *Gui) goEvery(g *gocui.Gui, interval time.Duration, function func(*gocui.Gui) error) { func (gui *Gui) goEvery(g *gocui.Gui, interval time.Duration, function func(*gocui.Gui) error) {
go func() { go func() {
for range time.Tick(interval) { for range time.Tick(interval) {

View File

@ -1,6 +1,8 @@
package gui package gui
import "github.com/jesseduffield/gocui" import (
"github.com/jesseduffield/gocui"
)
// Binding - a keybinding mapping a key and modifier to a handler. The keypress // Binding - a keybinding mapping a key and modifier to a handler. The keypress
// is only handled if the given view has focus, or handled globally if the view // is only handled if the given view has focus, or handled globally if the view
@ -10,73 +12,377 @@ type Binding struct {
Handler func(*gocui.Gui, *gocui.View) error Handler func(*gocui.Gui, *gocui.View) error
Key interface{} // FIXME: find out how to get `gocui.Key | rune` Key interface{} // FIXME: find out how to get `gocui.Key | rune`
Modifier gocui.Modifier Modifier gocui.Modifier
KeyReadable string
Description string
} }
func (gui *Gui) keybindings(g *gocui.Gui) error { // GetDisplayStrings returns the display string of a file
bindings := []Binding{ func (b *Binding) GetDisplayStrings() []string {
{ViewName: "", Key: 'q', Modifier: gocui.ModNone, Handler: gui.quit}, return []string{b.GetKey(), b.Description}
{ViewName: "", Key: gocui.KeyCtrlC, Modifier: gocui.ModNone, Handler: gui.quit}, }
{ViewName: "", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: gui.quit},
{ViewName: "", Key: gocui.KeyPgup, Modifier: gocui.ModNone, Handler: gui.scrollUpMain}, func (b *Binding) GetKey() string {
{ViewName: "", Key: gocui.KeyPgdn, Modifier: gocui.ModNone, Handler: gui.scrollDownMain}, r, ok := b.Key.(rune)
{ViewName: "", Key: gocui.KeyCtrlU, Modifier: gocui.ModNone, Handler: gui.scrollUpMain}, key := ""
{ViewName: "", Key: gocui.KeyCtrlD, Modifier: gocui.ModNone, Handler: gui.scrollDownMain},
{ViewName: "", Key: 'P', Modifier: gocui.ModNone, Handler: gui.pushFiles}, if ok {
{ViewName: "", Key: 'p', Modifier: gocui.ModNone, Handler: gui.pullFiles}, key = string(r)
{ViewName: "", Key: 'R', Modifier: gocui.ModNone, Handler: gui.handleRefresh}, } else if b.KeyReadable != "" {
{ViewName: "status", Key: 'e', Modifier: gocui.ModNone, Handler: gui.handleEditConfig}, key = b.KeyReadable
{ViewName: "status", Key: 'o', Modifier: gocui.ModNone, Handler: gui.handleOpenConfig}, }
{ViewName: "status", Key: 'u', Modifier: gocui.ModNone, Handler: gui.handleCheckForUpdate},
{ViewName: "status", Key: 's', Modifier: gocui.ModNone, Handler: gui.handleSwitchRepo}, return key
{ViewName: "files", Key: 'c', Modifier: gocui.ModNone, Handler: gui.handleCommitPress}, }
{ViewName: "files", Key: 'C', Modifier: gocui.ModNone, Handler: gui.handleCommitEditorPress},
{ViewName: "files", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handleFilePress}, func (gui *Gui) GetKeybindings() []*Binding {
{ViewName: "files", Key: 'd', Modifier: gocui.ModNone, Handler: gui.handleFileRemove}, bindings := []*Binding{
{ViewName: "files", Key: 'm', Modifier: gocui.ModNone, Handler: gui.handleSwitchToMerge}, {
{ViewName: "files", Key: 'e', Modifier: gocui.ModNone, Handler: gui.handleFileEdit}, ViewName: "",
{ViewName: "files", Key: 'o', Modifier: gocui.ModNone, Handler: gui.handleFileOpen}, Key: 'q',
{ViewName: "files", Key: 'i', Modifier: gocui.ModNone, Handler: gui.handleIgnoreFile}, Modifier: gocui.ModNone,
{ViewName: "files", Key: 'r', Modifier: gocui.ModNone, Handler: gui.handleRefreshFiles}, Handler: gui.quit,
{ViewName: "files", Key: 'S', Modifier: gocui.ModNone, Handler: gui.handleStashSave}, }, {
{ViewName: "files", Key: 'A', Modifier: gocui.ModNone, Handler: gui.handleAbortMerge}, ViewName: "",
{ViewName: "files", Key: 'a', Modifier: gocui.ModNone, Handler: gui.handleStageAll}, Key: gocui.KeyCtrlC,
{ViewName: "files", Key: 't', Modifier: gocui.ModNone, Handler: gui.handleAddPatch}, Modifier: gocui.ModNone,
{ViewName: "files", Key: 'D', Modifier: gocui.ModNone, Handler: gui.handleResetHard}, Handler: gui.quit,
{ViewName: "main", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: gui.handleEscapeMerge}, }, {
{ViewName: "main", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handlePickHunk}, ViewName: "",
{ViewName: "main", Key: 'b', Modifier: gocui.ModNone, Handler: gui.handlePickBothHunks}, Key: gocui.KeyEsc,
{ViewName: "main", Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.handleSelectPrevConflict}, Modifier: gocui.ModNone,
{ViewName: "main", Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: gui.handleSelectNextConflict}, Handler: gui.quit,
{ViewName: "main", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: gui.handleSelectTop}, }, {
{ViewName: "main", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: gui.handleSelectBottom}, ViewName: "",
{ViewName: "main", Key: 'h', Modifier: gocui.ModNone, Handler: gui.handleSelectPrevConflict}, Key: gocui.KeyPgup,
{ViewName: "main", Key: 'l', Modifier: gocui.ModNone, Handler: gui.handleSelectNextConflict}, Modifier: gocui.ModNone,
{ViewName: "main", Key: 'k', Modifier: gocui.ModNone, Handler: gui.handleSelectTop}, Handler: gui.scrollUpMain,
{ViewName: "main", Key: 'j', Modifier: gocui.ModNone, Handler: gui.handleSelectBottom}, }, {
{ViewName: "main", Key: 'z', Modifier: gocui.ModNone, Handler: gui.handlePopFileSnapshot}, ViewName: "",
{ViewName: "branches", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handleBranchPress}, Key: gocui.KeyPgdn,
{ViewName: "branches", Key: 'c', Modifier: gocui.ModNone, Handler: gui.handleCheckoutByName}, Modifier: gocui.ModNone,
{ViewName: "branches", Key: 'F', Modifier: gocui.ModNone, Handler: gui.handleForceCheckout}, Handler: gui.scrollDownMain,
{ViewName: "branches", Key: 'n', Modifier: gocui.ModNone, Handler: gui.handleNewBranch}, }, {
{ViewName: "branches", Key: 'd', Modifier: gocui.ModNone, Handler: gui.handleDeleteBranch}, ViewName: "",
{ViewName: "branches", Key: 'D', Modifier: gocui.ModNone, Handler: gui.handleForceDeleteBranch}, Key: gocui.KeyCtrlU,
{ViewName: "branches", Key: 'm', Modifier: gocui.ModNone, Handler: gui.handleMerge}, Modifier: gocui.ModNone,
{ViewName: "commits", Key: 's', Modifier: gocui.ModNone, Handler: gui.handleCommitSquashDown}, Handler: gui.scrollUpMain,
{ViewName: "commits", Key: 'r', Modifier: gocui.ModNone, Handler: gui.handleRenameCommit}, }, {
{ViewName: "commits", Key: 'R', Modifier: gocui.ModNone, Handler: gui.handleRenameCommitEditor}, ViewName: "",
{ViewName: "commits", Key: 'g', Modifier: gocui.ModNone, Handler: gui.handleResetToCommit}, Key: gocui.KeyCtrlD,
{ViewName: "commits", Key: 'f', Modifier: gocui.ModNone, Handler: gui.handleCommitFixup}, Modifier: gocui.ModNone,
{ViewName: "stash", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handleStashApply}, Handler: gui.scrollDownMain,
{ViewName: "stash", Key: 'g', Modifier: gocui.ModNone, Handler: gui.handleStashPop}, }, {
{ViewName: "stash", Key: 'd', Modifier: gocui.ModNone, Handler: gui.handleStashDrop}, ViewName: "",
{ViewName: "commitMessage", Key: gocui.KeyEnter, Modifier: gocui.ModNone, Handler: gui.handleCommitConfirm}, Key: 'P',
{ViewName: "commitMessage", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: gui.handleCommitClose}, Modifier: gocui.ModNone,
Handler: gui.pushFiles,
Description: gui.Tr.SLocalize("push"),
}, {
ViewName: "",
Key: 'p',
Modifier: gocui.ModNone,
Handler: gui.pullFiles,
Description: gui.Tr.SLocalize("pull"),
}, {
ViewName: "",
Key: 'R',
Modifier: gocui.ModNone,
Handler: gui.handleRefresh,
Description: gui.Tr.SLocalize("refresh"),
}, {
ViewName: "",
Key: 'x',
Modifier: gocui.ModNone,
Handler: gui.handleCreateOptionsMenu,
}, {
ViewName: "status",
Key: 'e',
Modifier: gocui.ModNone,
Handler: gui.handleEditConfig,
Description: gui.Tr.SLocalize("EditConfig"),
}, {
ViewName: "status",
Key: 'o',
Modifier: gocui.ModNone,
Handler: gui.handleOpenConfig,
Description: gui.Tr.SLocalize("OpenConfig"),
}, {
ViewName: "status",
Key: 'u',
Modifier: gocui.ModNone,
Handler: gui.handleCheckForUpdate,
Description: gui.Tr.SLocalize("checkForUpdate"),
}, {
ViewName: "status",
Key: 's',
Modifier: gocui.ModNone,
Handler: gui.handleSwitchRepo,
Description: gui.Tr.SLocalize("SwitchRepo"),
},
{
ViewName: "files",
Key: 'c',
Modifier: gocui.ModNone,
Handler: gui.handleCommitPress,
Description: gui.Tr.SLocalize("CommitChanges"),
}, {
ViewName: "files",
Key: 'C',
Modifier: gocui.ModNone,
Handler: gui.handleCommitEditorPress,
Description: gui.Tr.SLocalize("CommitChangesWithEditor"),
}, {
ViewName: "files",
Key: gocui.KeySpace,
Modifier: gocui.ModNone,
Handler: gui.handleFilePress,
KeyReadable: "space",
Description: gui.Tr.SLocalize("toggleStaged"),
}, {
ViewName: "files",
Key: 'd',
Modifier: gocui.ModNone,
Handler: gui.handleFileRemove,
Description: gui.Tr.SLocalize("removeFile"),
}, {
ViewName: "files",
Key: 'm',
Modifier: gocui.ModNone,
Handler: gui.handleSwitchToMerge,
Description: gui.Tr.SLocalize("resolveMergeConflicts"),
}, {
ViewName: "files",
Key: 'e',
Modifier: gocui.ModNone,
Handler: gui.handleFileEdit,
Description: gui.Tr.SLocalize("editFile"),
}, {
ViewName: "files",
Key: 'o',
Modifier: gocui.ModNone,
Handler: gui.handleFileOpen,
Description: gui.Tr.SLocalize("openFile"),
}, {
ViewName: "files",
Key: 'i',
Modifier: gocui.ModNone,
Handler: gui.handleIgnoreFile,
Description: gui.Tr.SLocalize("ignoreFile"),
}, {
ViewName: "files",
Key: 'r',
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: 'A',
Modifier: gocui.ModNone,
Handler: gui.handleAbortMerge,
Description: gui.Tr.SLocalize("abortMerge"),
}, {
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.handleResetHard,
Description: gui.Tr.SLocalize("resetHard"),
}, {
ViewName: "main",
Key: gocui.KeyEsc,
Modifier: gocui.ModNone,
Handler: gui.handleEscapeMerge,
}, {
ViewName: "main",
Key: gocui.KeySpace,
Modifier: gocui.ModNone,
Handler: gui.handlePickHunk,
}, {
ViewName: "main",
Key: 'b',
Modifier: gocui.ModNone,
Handler: gui.handlePickBothHunks,
}, {
ViewName: "main",
Key: gocui.KeyArrowLeft,
Modifier: gocui.ModNone,
Handler: gui.handleSelectPrevConflict,
}, {
ViewName: "main",
Key: gocui.KeyArrowRight,
Modifier: gocui.ModNone,
Handler: gui.handleSelectNextConflict,
}, {
ViewName: "main",
Key: gocui.KeyArrowUp,
Modifier: gocui.ModNone,
Handler: gui.handleSelectTop,
}, {
ViewName: "main",
Key: gocui.KeyArrowDown,
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,
}, {
ViewName: "branches",
Key: gocui.KeySpace,
Modifier: gocui.ModNone,
Handler: gui.handleBranchPress,
KeyReadable: "space",
Description: gui.Tr.SLocalize("checkout"),
}, {
ViewName: "branches",
Key: 'c',
Modifier: gocui.ModNone,
Handler: gui.handleCheckoutByName,
Description: gui.Tr.SLocalize("checkoutByName"),
}, {
ViewName: "branches",
Key: 'F',
Modifier: gocui.ModNone,
Handler: gui.handleForceCheckout,
Description: gui.Tr.SLocalize("forceCheckout"),
}, {
ViewName: "branches",
Key: 'n',
Modifier: gocui.ModNone,
Handler: gui.handleNewBranch,
Description: gui.Tr.SLocalize("newBranch"),
}, {
ViewName: "branches",
Key: 'd',
Modifier: gocui.ModNone,
Handler: gui.handleDeleteBranch,
Description: gui.Tr.SLocalize("deleteBranch"),
}, {
ViewName: "branches",
Key: 'D',
Modifier: gocui.ModNone,
Handler: gui.handleForceDeleteBranch,
Description: gui.Tr.SLocalize("forceDeleteBranch"),
}, {
ViewName: "branches",
Key: 'm',
Modifier: gocui.ModNone,
Handler: gui.handleMerge,
Description: gui.Tr.SLocalize("mergeIntoCurrentBranch"),
}, {
ViewName: "commits",
Key: 's',
Modifier: gocui.ModNone,
Handler: gui.handleCommitSquashDown,
Description: gui.Tr.SLocalize("squashDown"),
}, {
ViewName: "commits",
Key: 'r',
Modifier: gocui.ModNone,
Handler: gui.handleRenameCommit,
Description: gui.Tr.SLocalize("renameCommit"),
}, {
ViewName: "commits",
Key: 'R',
Modifier: gocui.ModNone,
Handler: gui.handleRenameCommitEditor,
Description: gui.Tr.SLocalize("renameCommitEditor"),
}, {
ViewName: "commits",
Key: 'g',
Modifier: gocui.ModNone,
Handler: gui.handleResetToCommit,
Description: gui.Tr.SLocalize("resetToThisCommit"),
}, {
ViewName: "commits",
Key: 'f',
Modifier: gocui.ModNone,
Handler: gui.handleCommitFixup,
Description: gui.Tr.SLocalize("fixupCommit"),
}, {
ViewName: "stash",
Key: gocui.KeySpace,
Modifier: gocui.ModNone,
Handler: gui.handleStashApply,
KeyReadable: "space",
Description: gui.Tr.SLocalize("apply"),
}, {
ViewName: "stash",
Key: 'g',
Modifier: gocui.ModNone,
Handler: gui.handleStashPop,
Description: gui.Tr.SLocalize("pop"),
}, {
ViewName: "stash",
Key: 'd',
Modifier: gocui.ModNone,
Handler: gui.handleStashDrop,
Description: gui.Tr.SLocalize("drop"),
}, {
ViewName: "commitMessage",
Key: gocui.KeyEnter,
Modifier: gocui.ModNone,
Handler: gui.handleCommitConfirm,
}, {
ViewName: "commitMessage",
Key: gocui.KeyEsc,
Modifier: gocui.ModNone,
Handler: gui.handleCommitClose,
}, {
ViewName: "menu",
Key: gocui.KeyEsc,
Modifier: gocui.ModNone,
Handler: gui.handleMenuClose,
}, {
ViewName: "menu",
Key: 'q',
Modifier: gocui.ModNone,
Handler: gui.handleMenuClose,
},
} }
// Would make these keybindings global but that interferes with editing // Would make these keybindings global but that interferes with editing
// input in the confirmation panel // input in the confirmation panel
for _, viewName := range []string{"status", "files", "branches", "commits", "stash"} { for _, viewName := range []string{"status", "files", "branches", "commits", "stash", "menu"} {
bindings = append(bindings, []Binding{ bindings = append(bindings, []*Binding{
{ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.nextView}, {ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.nextView},
{ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.previousView}, {ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.previousView},
{ViewName: viewName, Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: gui.nextView}, {ViewName: viewName, Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: gui.nextView},
@ -89,6 +395,12 @@ func (gui *Gui) keybindings(g *gocui.Gui) error {
}...) }...)
} }
return bindings
}
func (gui *Gui) keybindings(g *gocui.Gui) error {
bindings := gui.GetKeybindings()
for _, binding := range bindings { for _, binding := range bindings {
if err := g.SetKeybinding(binding.ViewName, binding.Key, binding.Modifier, binding.Handler); err != nil { if err := g.SetKeybinding(binding.ViewName, binding.Key, binding.Modifier, binding.Handler); err != nil {
return err return err

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

@ -0,0 +1,71 @@
package gui
import (
"fmt"
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) handleMenuSelect(g *gocui.Gui, v *gocui.View) error {
// doing nothing for now
// but it is needed for switch in newLineFocused
return nil
}
func (gui *Gui) renderMenuOptions(g *gocui.Gui) error {
optionsMap := map[string]string{
"esc/q": gui.Tr.SLocalize("close"),
"↑ ↓": gui.Tr.SLocalize("navigate"),
"space": gui.Tr.SLocalize("execute"),
}
return gui.renderOptionsMap(g, optionsMap)
}
func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error {
if err := g.DeleteKeybinding("menu", gocui.KeySpace, gocui.ModNone); err != nil {
return err
}
err := g.DeleteView("menu")
if err != nil {
return err
}
return gui.returnFocus(g, v)
}
func (gui *Gui) createMenu(items interface{}, handlePress func(int) error) error {
list, err := utils.RenderList(items)
if err != nil {
return err
}
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, list)
menuView, _ := gui.g.SetView("menu", x0, y0, x1, y1, 0)
menuView.Title = strings.Title(gui.Tr.SLocalize("menu"))
menuView.FgColor = gocui.ColorWhite
menuView.Clear()
fmt.Fprint(menuView, list)
if err := gui.renderMenuOptions(gui.g); err != nil {
return err
}
wrappedHandlePress := func(g *gocui.Gui, v *gocui.View) error {
lineNumber := gui.getItemPosition(v)
return handlePress(lineNumber)
}
if err := gui.g.SetKeybinding("menu", gocui.KeySpace, gocui.ModNone, wrappedHandlePress); err != nil {
return err
}
gui.g.Update(func(g *gocui.Gui) error {
if _, err := g.SetViewOnTop("menu"); err != nil {
return err
}
currentView := gui.g.CurrentView()
return gui.switchFocus(gui.g, currentView, menuView)
})
return nil
}

View File

@ -0,0 +1,51 @@
package gui
import (
"errors"
"github.com/jesseduffield/gocui"
)
func (gui *Gui) getBindings(v *gocui.View) []*Binding {
var (
bindingsGlobal, bindingsPanel []*Binding
)
bindings := gui.GetKeybindings()
for _, binding := range bindings {
if binding.GetKey() != "" && binding.Description != "" {
switch binding.ViewName {
case "":
bindingsGlobal = append(bindingsGlobal, binding)
case v.Name():
bindingsPanel = append(bindingsPanel, binding)
}
}
}
// append dummy element to have a separator between
// panel and global keybindings
bindingsPanel = append(bindingsPanel, &Binding{})
return append(bindingsPanel, bindingsGlobal...)
}
func (gui *Gui) handleCreateOptionsMenu(g *gocui.Gui, v *gocui.View) error {
bindings := gui.getBindings(v)
handleOptionsMenuPress := func(index int) error {
if bindings[index].Key == nil {
return nil
}
if index <= len(bindings) {
return errors.New("Index is greater than size of bindings")
}
err := gui.handleMenuClose(g, v)
if err != nil {
return err
}
return bindings[index].Handler(g, v)
}
return gui.createMenu(bindings, handleOptionsMenuPress)
}

View File

@ -5,6 +5,7 @@ import (
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
) )
func (gui *Gui) refreshStashEntries(g *gocui.Gui) error { func (gui *Gui) refreshStashEntries(g *gocui.Gui) error {
@ -14,10 +15,14 @@ func (gui *Gui) refreshStashEntries(g *gocui.Gui) error {
panic(err) panic(err)
} }
gui.State.StashEntries = gui.GitCommand.GetStashEntries() gui.State.StashEntries = gui.GitCommand.GetStashEntries()
v.Clear() v.Clear()
for _, stashEntry := range gui.State.StashEntries { list, err := utils.RenderList(gui.State.StashEntries)
fmt.Fprintln(v, stashEntry.DisplayString) if err != nil {
return err
} }
fmt.Fprint(v, list)
return gui.resetOrigin(v) return gui.resetOrigin(v)
}) })
return nil return nil
@ -27,17 +32,13 @@ func (gui *Gui) getSelectedStashEntry(v *gocui.View) *commands.StashEntry {
if len(gui.State.StashEntries) == 0 { if len(gui.State.StashEntries) == 0 {
return nil return nil
} }
lineNumber := gui.getItemPosition(v) stashView, _ := gui.g.View("stash")
return &gui.State.StashEntries[lineNumber] lineNumber := gui.getItemPosition(stashView)
return gui.State.StashEntries[lineNumber]
} }
func (gui *Gui) renderStashOptions(g *gocui.Gui) error { func (gui *Gui) renderStashOptions(g *gocui.Gui) error {
return gui.renderOptionsMap(g, map[string]string{ return gui.renderGlobalOptions(g)
"space": gui.Tr.SLocalize("apply"),
"g": gui.Tr.SLocalize("pop"),
"d": gui.Tr.SLocalize("drop"),
"← → ↑ ↓": gui.Tr.SLocalize("navigate"),
})
} }
func (gui *Gui) handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error {

View File

@ -42,11 +42,7 @@ func (gui *Gui) refreshStatus(g *gocui.Gui) error {
} }
func (gui *Gui) renderStatusOptions(g *gocui.Gui) error { func (gui *Gui) renderStatusOptions(g *gocui.Gui) error {
return gui.renderOptionsMap(g, map[string]string{ return gui.renderGlobalOptions(g)
"o": gui.Tr.SLocalize("OpenConfig"),
"e": gui.Tr.SLocalize("EditConfig"),
"u": gui.Tr.SLocalize("CheckForUpdate"),
})
} }
func (gui *Gui) handleCheckForUpdate(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) handleCheckForUpdate(g *gocui.Gui, v *gocui.View) error {

View File

@ -82,6 +82,8 @@ func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error {
mainView.SetOrigin(0, 0) mainView.SetOrigin(0, 0)
switch v.Name() { switch v.Name() {
case "menu":
return gui.handleMenuSelect(g, v)
case "status": case "status":
return gui.handleStatusSelect(g, v) return gui.handleStatusSelect(g, v)
case "files": case "files":
@ -145,6 +147,7 @@ func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error {
return err return err
} }
g.Cursor = newView.Editable g.Cursor = newView.Editable
return gui.newLineFocused(g, newView) return gui.newLineFocused(g, newView)
} }
@ -157,7 +160,6 @@ func (gui *Gui) getItemPosition(v *gocui.View) int {
func (gui *Gui) cursorUp(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) cursorUp(g *gocui.Gui, v *gocui.View) error {
// swallowing cursor movements in main // swallowing cursor movements in main
// TODO: pull this out
if v == nil || v.Name() == "main" { if v == nil || v.Name() == "main" {
return nil return nil
} }
@ -176,19 +178,28 @@ func (gui *Gui) cursorUp(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) cursorDown(g *gocui.Gui, v *gocui.View) error { func (gui *Gui) cursorDown(g *gocui.Gui, v *gocui.View) error {
// swallowing cursor movements in main // swallowing cursor movements in main
// TODO: pull this out
if v == nil || v.Name() == "main" { if v == nil || v.Name() == "main" {
return nil return nil
} }
cx, cy := v.Cursor() cx, cy := v.Cursor()
ox, oy := v.Origin() ox, oy := v.Origin()
if cy+oy >= len(v.BufferLines())-2 { ly := len(v.BufferLines()) - 1
_, height := v.Size()
maxY := height - 1
// if we are at the end we just return
if cy+oy == ly {
return nil return nil
} }
if err := v.SetCursor(cx, cy+1); err != nil {
if err := v.SetOrigin(ox, oy+1); err != nil { var err error
return err if cy < maxY {
err = v.SetCursor(cx, cy+1)
} else {
err = v.SetOrigin(ox, oy+1)
} }
if err != nil {
return err
} }
gui.newLineFocused(g, v) gui.newLineFocused(g, v)
@ -205,10 +216,19 @@ func (gui *Gui) resetOrigin(v *gocui.View) error {
// if the cursor down past the last item, move it to the last line // if the cursor down past the last item, move it to the last line
func (gui *Gui) correctCursor(v *gocui.View) error { func (gui *Gui) correctCursor(v *gocui.View) error {
cx, cy := v.Cursor() cx, cy := v.Cursor()
_, oy := v.Origin() ox, oy := v.Origin()
lineCount := len(v.BufferLines()) - 2 _, height := v.Size()
if cy >= lineCount-oy { maxY := height - 1
return v.SetCursor(cx, lineCount-oy) ly := len(v.BufferLines()) - 1
if oy+cy <= ly {
return nil
}
newCy := utils.Min(ly, maxY)
if err := v.SetCursor(cx, newCy); err != nil {
return err
}
if err := v.SetOrigin(ox, ly-newCy); err != nil {
return err
} }
return nil return nil
} }
@ -245,6 +265,7 @@ func (gui *Gui) renderOptionsMap(g *gocui.Gui, optionsMap map[string]string) err
} }
// TODO: refactor properly // TODO: refactor properly
// i'm so sorry but had to add this getBranchesView
func (gui *Gui) getFilesView(g *gocui.Gui) *gocui.View { func (gui *Gui) getFilesView(g *gocui.Gui) *gocui.View {
v, _ := g.View("files") v, _ := g.View("files")
return v return v
@ -260,6 +281,11 @@ func (gui *Gui) getCommitMessageView(g *gocui.Gui) *gocui.View {
return v return v
} }
func (gui *Gui) getBranchesView(g *gocui.Gui) *gocui.View {
v, _ := g.View("branches")
return v
}
func (gui *Gui) trimmedContent(v *gocui.View) string { func (gui *Gui) trimmedContent(v *gocui.View) string {
return strings.TrimSpace(v.Buffer()) return strings.TrimSpace(v.Buffer())
} }

View File

@ -34,12 +34,24 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{ }, &i18n.Message{
ID: "CommitChanges", ID: "CommitChanges",
Other: "Commit Veranderingen", Other: "Commit Veranderingen",
}, &i18n.Message{
ID: "CommitChangesWithEditor",
Other: "commit Veranderingen met de git editor",
}, &i18n.Message{ }, &i18n.Message{
ID: "StatusTitle", ID: "StatusTitle",
Other: "Status", Other: "Status",
}, &i18n.Message{
ID: "GlobalTitle",
Other: "Global",
}, &i18n.Message{ }, &i18n.Message{
ID: "navigate", ID: "navigate",
Other: "navigeer", Other: "navigeer",
}, &i18n.Message{
ID: "menu",
Other: "menu",
}, &i18n.Message{
ID: "execute",
Other: "uitvoeren",
}, &i18n.Message{ }, &i18n.Message{
ID: "stashFiles", ID: "stashFiles",
Other: "stash-bestanden", Other: "stash-bestanden",
@ -66,7 +78,7 @@ func addDutch(i18nObject *i18n.Bundle) error {
Other: "verandering toevoegen", Other: "verandering toevoegen",
}, &i18n.Message{ }, &i18n.Message{
ID: "edit", ID: "edit",
Other: "veranderen", Other: "verander",
}, &i18n.Message{ }, &i18n.Message{
ID: "scroll", ID: "scroll",
Other: "scroll", Other: "scroll",
@ -172,6 +184,9 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{ }, &i18n.Message{
ID: "CloseConfirm", ID: "CloseConfirm",
Other: "{{.keyBindClose}}: Sluiten, {{.keyBindConfirm}}: Bevestigen", Other: "{{.keyBindClose}}: Sluiten, {{.keyBindConfirm}}: Bevestigen",
}, &i18n.Message{
ID: "close",
Other: "sluiten",
}, &i18n.Message{ }, &i18n.Message{
ID: "SureResetThisCommit", ID: "SureResetThisCommit",
Other: "Weet je het zeker dat je wil resetten naar deze commit?", Other: "Weet je het zeker dat je wil resetten naar deze commit?",
@ -212,8 +227,11 @@ func addDutch(i18nObject *i18n.Bundle) error {
ID: "OnlyRenameTopCommit", ID: "OnlyRenameTopCommit",
Other: "Je kan alleen de bovenste commit hernoemen", Other: "Je kan alleen de bovenste commit hernoemen",
}, &i18n.Message{ }, &i18n.Message{
ID: "RenameCommit", ID: "renameCommit",
Other: "Hernoem Commit", Other: "hernoem commit",
}, &i18n.Message{
ID: "renameCommitEditor",
Other: "rename commit with editor",
}, &i18n.Message{ }, &i18n.Message{
ID: "PotentialErrInGetselectedCommit", ID: "PotentialErrInGetselectedCommit",
Other: "Er is mogelijk een error in getSelected Commit (geen match tussen ui en state)", Other: "Er is mogelijk een error in getSelected Commit (geen match tussen ui en state)",
@ -295,9 +313,63 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{ }, &i18n.Message{
ID: "MergeAborted", ID: "MergeAborted",
Other: "Merge afgebroken", Other: "Merge afgebroken",
}, &i18n.Message{
ID: "OpenConfig",
Other: "open config file",
}, &i18n.Message{
ID: "EditConfig",
Other: "verander config file",
}, &i18n.Message{
ID: "ForcePush",
Other: "Forceer push",
}, &i18n.Message{
ID: "ForcePushPrompt",
Other: "Jou branch is afgeweken van de remote branch. Druk 'esc' om te anuleren, of 'enter' om geforceert te pushen.",
}, &i18n.Message{
ID: "checkForUpdate",
Other: "check voor updates",
}, &i18n.Message{
ID: "CheckingForUpdates",
Other: "checken voor updates...",
}, &i18n.Message{
ID: "OnLatestVersionErr",
Other: "Je hebt al de laatste versie",
}, &i18n.Message{
ID: "MajorVersionErr",
Other: "Nieuwe versie ({{.newVersion}}) is niet teruggaand compatibele vergeleken met de huidige versie ({{.currentVersion}})",
}, &i18n.Message{
ID: "CouldNotFindBinaryErr",
Other: "Kon geen binary vinden op {{.url}}",
}, &i18n.Message{
ID: "AnonymousReportingTitle",
Other: "Help maak lazygit beter",
}, &i18n.Message{
ID: "AnonymousReportingPrompt",
Other: "Zou je anonieme data rapportage willen aanzetten om lazygit beter te kunnen maken? (enter/esc)",
}, &i18n.Message{
ID: "removeFile",
Other: `Verwijder als untracked / uitchecken wordt gevolgd (ga weg)`,
}, &i18n.Message{
ID: "editFile",
Other: `verander bestand`,
}, &i18n.Message{
ID: "openFile",
Other: `open bestand`,
}, &i18n.Message{
ID: "ignoreFile",
Other: `voeg toe aan .gitignore`,
}, &i18n.Message{
ID: "refreshFiles",
Other: `refresh bestanden`,
}, &i18n.Message{
ID: "resetHard",
Other: `harde reset`,
}, &i18n.Message{
ID: "mergeIntoCurrentBranch",
Other: `merge in met huidige checked out branch`,
}, &i18n.Message{ }, &i18n.Message{
ID: "ConfirmQuit", ID: "ConfirmQuit",
Other: `Are you sure you want to quit?`, Other: `Weet je zeker dat je dit programma wil sluiten?`,
}, },
) )
} }

View File

@ -42,12 +42,24 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{ }, &i18n.Message{
ID: "CommitChanges", ID: "CommitChanges",
Other: "commit changes", Other: "commit changes",
}, &i18n.Message{
ID: "CommitChangesWithEditor",
Other: "commit changes using git editor",
}, &i18n.Message{ }, &i18n.Message{
ID: "StatusTitle", ID: "StatusTitle",
Other: "Status", Other: "Status",
}, &i18n.Message{
ID: "GlobalTitle",
Other: "Global",
}, &i18n.Message{ }, &i18n.Message{
ID: "navigate", ID: "navigate",
Other: "navigate", Other: "navigate",
}, &i18n.Message{
ID: "menu",
Other: "menu",
}, &i18n.Message{
ID: "execute",
Other: "execute",
}, &i18n.Message{ }, &i18n.Message{
ID: "stashFiles", ID: "stashFiles",
Other: "stash files", Other: "stash files",
@ -69,6 +81,12 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{ }, &i18n.Message{
ID: "refresh", ID: "refresh",
Other: "refresh", Other: "refresh",
}, &i18n.Message{
ID: "push",
Other: "push",
}, &i18n.Message{
ID: "pull",
Other: "pull",
}, &i18n.Message{ }, &i18n.Message{
ID: "addPatch", ID: "addPatch",
Other: "add patch", Other: "add patch",
@ -183,6 +201,9 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{ }, &i18n.Message{
ID: "CloseConfirm", ID: "CloseConfirm",
Other: "{{.keyBindClose}}: close, {{.keyBindConfirm}}: confirm", Other: "{{.keyBindClose}}: close, {{.keyBindConfirm}}: confirm",
}, &i18n.Message{
ID: "close",
Other: "close",
}, &i18n.Message{ }, &i18n.Message{
ID: "SureResetThisCommit", ID: "SureResetThisCommit",
Other: "Are you sure you want to reset to this commit?", Other: "Are you sure you want to reset to this commit?",
@ -223,8 +244,11 @@ func addEnglish(i18nObject *i18n.Bundle) error {
ID: "OnlyRenameTopCommit", ID: "OnlyRenameTopCommit",
Other: "Can only rename topmost commit", Other: "Can only rename topmost commit",
}, &i18n.Message{ }, &i18n.Message{
ID: "RenameCommit", ID: "renameCommit",
Other: "Rename Commit", Other: "rename commit",
}, &i18n.Message{
ID: "renameCommitEditor",
Other: "rename commit with editor",
}, &i18n.Message{ }, &i18n.Message{
ID: "PotentialErrInGetselectedCommit", ID: "PotentialErrInGetselectedCommit",
Other: "potential error in getSelected Commit (mismatched ui and state)", Other: "potential error in getSelected Commit (mismatched ui and state)",
@ -319,8 +343,8 @@ func addEnglish(i18nObject *i18n.Bundle) error {
ID: "ForcePushPrompt", ID: "ForcePushPrompt",
Other: "Your branch has diverged from the remote branch. Press 'esc' to cancel, or 'enter' to force push.", Other: "Your branch has diverged from the remote branch. Press 'esc' to cancel, or 'enter' to force push.",
}, &i18n.Message{ }, &i18n.Message{
ID: "CheckForUpdate", ID: "checkForUpdate",
Other: "Check for update", Other: "check for update",
}, &i18n.Message{ }, &i18n.Message{
ID: "CheckingForUpdates", ID: "CheckingForUpdates",
Other: "Checking for updates...", Other: "Checking for updates...",
@ -342,9 +366,33 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{ }, &i18n.Message{
ID: "GitconfigParseErr", ID: "GitconfigParseErr",
Other: `Gogit failed to parse your gitconfig file due to the presence of unquoted '\' characters. Removing these should fix the issue.`, 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`,
}, &i18n.Message{
ID: "openFile",
Other: `open file`,
}, &i18n.Message{
ID: "ignoreFile",
Other: `add to .gitignore`,
}, &i18n.Message{
ID: "refreshFiles",
Other: `refresh files`,
}, &i18n.Message{
ID: "resetHard",
Other: `reset hard`,
}, &i18n.Message{
ID: "mergeIntoCurrentBranch",
Other: `merge into currently checked out branch`,
}, &i18n.Message{ }, &i18n.Message{
ID: "ConfirmQuit", ID: "ConfirmQuit",
Other: `Are you sure you want to quit?`, Other: `Are you sure you want to quit?`,
}, &i18n.Message{
ID: "SwitchRepo",
Other: `Switch to a recent repo`,
}, },
) )
} }

View File

@ -32,12 +32,24 @@ func addPolish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{ }, &i18n.Message{
ID: "CommitChanges", ID: "CommitChanges",
Other: "commituj zmiany", Other: "commituj zmiany",
}, &i18n.Message{
ID: "CommitChangesWithEditor",
Other: "commituj zmiany używając edytora z gita",
}, &i18n.Message{ }, &i18n.Message{
ID: "StatusTitle", ID: "StatusTitle",
Other: "Status", Other: "Status",
}, &i18n.Message{
ID: "GlobalTitle",
Other: "Globalne",
}, &i18n.Message{ }, &i18n.Message{
ID: "navigate", ID: "navigate",
Other: "nawiguj", Other: "nawiguj",
}, &i18n.Message{
ID: "menu",
Other: "menu",
}, &i18n.Message{
ID: "execute",
Other: "wykonaj",
}, &i18n.Message{ }, &i18n.Message{
ID: "stashFiles", ID: "stashFiles",
Other: "przechowaj pliki", Other: "przechowaj pliki",
@ -134,6 +146,9 @@ func addPolish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{ }, &i18n.Message{
ID: "DeleteBranchMessage", ID: "DeleteBranchMessage",
Other: "Jesteś pewien, że chcesz usunąć gałąź {{.selectedBranchName}} ?", Other: "Jesteś pewien, że chcesz usunąć gałąź {{.selectedBranchName}} ?",
}, &i18n.Message{
ID: "ForceDeleteBranchMessage",
Other: "Na pewno wymusić usunięcie gałęzi {{.selectedBranchName}}?",
}, &i18n.Message{ }, &i18n.Message{
ID: "CantMergeBranchIntoItself", ID: "CantMergeBranchIntoItself",
Other: "Nie możesz scalić gałęzi do samej siebie", Other: "Nie możesz scalić gałęzi do samej siebie",
@ -152,6 +167,9 @@ func addPolish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{ }, &i18n.Message{
ID: "deleteBranch", ID: "deleteBranch",
Other: "usuń gałąź", Other: "usuń gałąź",
}, &i18n.Message{
ID: "forceDeleteBranch",
Other: "usuń gałąź (wymuś)",
}, &i18n.Message{ }, &i18n.Message{
ID: "NoBranchesThisRepo", ID: "NoBranchesThisRepo",
Other: "Brak gałęzi dla tego repozytorium", Other: "Brak gałęzi dla tego repozytorium",
@ -164,6 +182,9 @@ func addPolish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{ }, &i18n.Message{
ID: "CloseConfirm", ID: "CloseConfirm",
Other: "{{.keyBindClose}}: zamknij, {{.keyBindConfirm}}: potwierdź", Other: "{{.keyBindClose}}: zamknij, {{.keyBindConfirm}}: potwierdź",
}, &i18n.Message{
ID: "close",
Other: "zamknij",
}, &i18n.Message{ }, &i18n.Message{
ID: "SureResetThisCommit", ID: "SureResetThisCommit",
Other: "Jesteś pewny, że chcesz zresetować ten commit?", Other: "Jesteś pewny, że chcesz zresetować ten commit?",
@ -204,8 +225,11 @@ func addPolish(i18nObject *i18n.Bundle) error {
ID: "OnlyRenameTopCommit", ID: "OnlyRenameTopCommit",
Other: "Można przmianować tylko najwyższy commit", Other: "Można przmianować tylko najwyższy commit",
}, &i18n.Message{ }, &i18n.Message{
ID: "RenameCommit", ID: "renameCommit",
Other: "Przemianuj commit", Other: "przemianuj commit",
}, &i18n.Message{
ID: "renameCommitEditor",
Other: "przemianuj commit w edytorze",
}, &i18n.Message{ }, &i18n.Message{
ID: "PotentialErrInGetselectedCommit", ID: "PotentialErrInGetselectedCommit",
Other: "potencjalny błąd w getSelected Commit (niedopasowane ui i stan)", Other: "potencjalny błąd w getSelected Commit (niedopasowane ui i stan)",
@ -287,6 +311,60 @@ func addPolish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{ }, &i18n.Message{
ID: "MergeAborted", ID: "MergeAborted",
Other: "Scalanie anulowane", Other: "Scalanie anulowane",
}, &i18n.Message{
ID: "OpenConfig",
Other: "otwórz plik konfiguracyjny",
}, &i18n.Message{
ID: "EditConfig",
Other: "edytuj plik konfiguracyjny",
}, &i18n.Message{
ID: "ForcePush",
Other: "Wymuś wypchnięcie",
}, &i18n.Message{
ID: "ForcePushPrompt",
Other: "Twoja gałąź rozeszła się z gałęzią zdalną. Wciśnij 'esc' aby anulować lub 'enter' aby wymusić wypchnięcie.",
}, &i18n.Message{
ID: "checkForUpdate",
Other: "sprawdź aktualizacje",
}, &i18n.Message{
ID: "CheckingForUpdates",
Other: "Sprawdzanie aktualizacji...",
}, &i18n.Message{
ID: "OnLatestVersionErr",
Other: "Już posiadasz najnowszą wersję",
}, &i18n.Message{
ID: "MajorVersionErr",
Other: "Nowa wersja ({{.newVersion}}) posiada niekompatybilne zmiany w porównaniu do obecnej wersji ({{.currentVersion}})",
}, &i18n.Message{
ID: "CouldNotFindBinaryErr",
Other: "Nie można znaleźć pliku binarnego w {{.url}}",
}, &i18n.Message{
ID: "AnonymousReportingTitle",
Other: "Help make lazygit better",
}, &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`,
}, &i18n.Message{
ID: "openFile",
Other: `otwórz plik`,
}, &i18n.Message{
ID: "ignoreFile",
Other: `dodaj do .gitignore`,
}, &i18n.Message{
ID: "refreshFiles",
Other: `odśwież pliki`,
}, &i18n.Message{
ID: "resetHard",
Other: `zresetuj twardo`,
}, &i18n.Message{
ID: "mergeIntoCurrentBranch",
Other: `scal do obecnej gałęzi`,
}, &i18n.Message{ }, &i18n.Message{
ID: "ConfirmQuit", ID: "ConfirmQuit",
Other: `Na pewno chcesz wyjść z programu?`, Other: `Na pewno chcesz wyjść z programu?`,

View File

@ -1,10 +1,12 @@
package utils package utils
import ( import (
"errors"
"fmt" "fmt"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"reflect"
"strings" "strings"
"time" "time"
@ -99,3 +101,106 @@ func ResolvePlaceholderString(str string, arguments map[string]string) string {
} }
return str return str
} }
// Min returns the minimum of two integers
func Min(x, y int) int {
if x < y {
return x
}
return y
}
type Displayable interface {
GetDisplayStrings() []string
}
// RenderList takes a slice of items, confirms they implement the Displayable
// interface, then generates a list of their displaystrings to write to a panel's
// buffer
func RenderList(slice interface{}) (string, error) {
s := reflect.ValueOf(slice)
if s.Kind() != reflect.Slice {
return "", errors.New("RenderList given a non-slice type")
}
displayables := make([]Displayable, s.Len())
for i := 0; i < s.Len(); i++ {
value, ok := s.Index(i).Interface().(Displayable)
if !ok {
return "", errors.New("item does not implement the Displayable interface")
}
displayables[i] = value
}
return renderDisplayableList(displayables)
}
// renderDisplayableList takes a list of displayable items, obtains their display
// strings via GetDisplayStrings() and then returns a single string containing
// each item's string representation on its own line, with appropriate horizontal
// padding between the item's own strings
func renderDisplayableList(items []Displayable) (string, error) {
if len(items) == 0 {
return "", nil
}
stringArrays := getDisplayStringArrays(items)
if !displayArraysAligned(stringArrays) {
return "", errors.New("Each item must return the same number of strings to display")
}
padWidths := getPadWidths(stringArrays)
paddedDisplayStrings := getPaddedDisplayStrings(stringArrays, padWidths)
return strings.Join(paddedDisplayStrings, "\n"), nil
}
func getPadWidths(stringArrays [][]string) []int {
if len(stringArrays[0]) <= 1 {
return []int{}
}
padWidths := make([]int, len(stringArrays[0])-1)
for i := range padWidths {
for _, strings := range stringArrays {
if len(strings[i]) > padWidths[i] {
padWidths[i] = len(strings[i])
}
}
}
return padWidths
}
func getPaddedDisplayStrings(stringArrays [][]string, padWidths []int) []string {
paddedDisplayStrings := make([]string, len(stringArrays))
for i, stringArray := range stringArrays {
if len(stringArray) == 0 {
continue
}
for j, padWidth := range padWidths {
paddedDisplayStrings[i] += WithPadding(stringArray[j], padWidth) + " "
}
paddedDisplayStrings[i] += stringArray[len(padWidths)]
}
return paddedDisplayStrings
}
// displayArraysAligned returns true if every string array returned from our
// list of displayables has the same length
func displayArraysAligned(stringArrays [][]string) bool {
for _, strings := range stringArrays {
if len(strings) != len(stringArrays[0]) {
return false
}
}
return true
}
func getDisplayStringArrays(displayables []Displayable) [][]string {
stringArrays := make([][]string, len(displayables))
for i, item := range displayables {
stringArrays[i] = item.GetDisplayStrings()
}
return stringArrays
}

View File

@ -1,6 +1,7 @@
package utils package utils
import ( import (
"errors"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -167,3 +168,181 @@ func TestResolvePlaceholderString(t *testing.T) {
assert.EqualValues(t, string(s.expected), ResolvePlaceholderString(s.templateString, s.arguments)) assert.EqualValues(t, string(s.expected), ResolvePlaceholderString(s.templateString, s.arguments))
} }
} }
func TestDisplayArraysAligned(t *testing.T) {
type scenario struct {
input [][]string
expected bool
}
scenarios := []scenario{
{
[][]string{{"", ""}, {"", ""}},
true,
},
{
[][]string{{""}, {"", ""}},
false,
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, displayArraysAligned(s.input))
}
}
type myDisplayable struct {
strings []string
}
type myStruct struct{}
func (d *myDisplayable) GetDisplayStrings() []string {
return d.strings
}
func TestGetDisplayStringArrays(t *testing.T) {
type scenario struct {
input []Displayable
expected [][]string
}
scenarios := []scenario{
{
[]Displayable{
Displayable(&myDisplayable{[]string{"a", "b"}}),
Displayable(&myDisplayable{[]string{"c", "d"}}),
},
[][]string{{"a", "b"}, {"c", "d"}},
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, getDisplayStringArrays(s.input))
}
}
func TestRenderDisplayableList(t *testing.T) {
type scenario struct {
input []Displayable
expectedString string
expectedError error
}
scenarios := []scenario{
{
[]Displayable{
Displayable(&myDisplayable{[]string{}}),
Displayable(&myDisplayable{[]string{}}),
},
"\n",
nil,
},
{
[]Displayable{
Displayable(&myDisplayable{[]string{"aa", "b"}}),
Displayable(&myDisplayable{[]string{"c", "d"}}),
},
"aa b\nc d",
nil,
},
{
[]Displayable{
Displayable(&myDisplayable{[]string{"a"}}),
Displayable(&myDisplayable{[]string{"b", "c"}}),
},
"",
errors.New("Each item must return the same number of strings to display"),
},
}
for _, s := range scenarios {
str, err := renderDisplayableList(s.input)
assert.EqualValues(t, s.expectedString, str)
assert.EqualValues(t, s.expectedError, err)
}
}
func TestRenderList(t *testing.T) {
type scenario struct {
input interface{}
expectedString string
expectedError error
}
scenarios := []scenario{
{
[]*myDisplayable{
{[]string{"aa", "b"}},
{[]string{"c", "d"}},
},
"aa b\nc d",
nil,
},
{
[]*myStruct{
{},
{},
},
"",
errors.New("item does not implement the Displayable interface"),
},
{
&myStruct{},
"",
errors.New("RenderList given a non-slice type"),
},
}
for _, s := range scenarios {
str, err := RenderList(s.input)
assert.EqualValues(t, s.expectedString, str)
assert.EqualValues(t, s.expectedError, err)
}
}
func TestGetPaddedDisplayStrings(t *testing.T) {
type scenario struct {
stringArrays [][]string
padWidths []int
expected []string
}
scenarios := []scenario{
{
[][]string{{"a", "b"}, {"c", "d"}},
[]int{1},
[]string{"a b", "c d"},
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, getPaddedDisplayStrings(s.stringArrays, s.padWidths))
}
}
func TestGetPadWidths(t *testing.T) {
type scenario struct {
stringArrays [][]string
expected []int
}
scenarios := []scenario{
{
[][]string{{""}, {""}},
[]int{},
},
{
[][]string{{"a"}, {""}},
[]int{},
},
{
[][]string{{"aa", "b", "ccc"}, {"c", "d", "e"}},
[]int{2, 1},
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, getPadWidths(s.stringArrays))
}
}

View File

@ -0,0 +1,54 @@
// run:
// LANG=en go run generate_cheatsheet.go
// to generate Keybindings_en.md file in current directory
// change LANG to generate cheatsheet in different language (if supported)
package main
import (
"fmt"
"os"
"strings"
"github.com/jesseduffield/lazygit/pkg/app"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func main() {
appConfig, _ := config.NewAppConfig("", "", "", "", "", new(bool))
a, _ := app.NewApp(appConfig)
lang := a.Tr.GetLanguage()
name := "Keybindings_" + lang + ".md"
bindings := a.Gui.GetKeybindings()
padWidth := a.Gui.GetMaxKeyLength(bindings)
file, _ := os.Create(name)
current := "v"
content := ""
title := ""
file.WriteString("# Lazygit " + a.Tr.SLocalize("menu"))
for _, binding := range bindings {
if key := a.Gui.GetKey(binding); key != "" && (binding.Description != "" || key == "x") {
if binding.ViewName != current {
current = binding.ViewName
if current == "" {
title = a.Tr.SLocalize("GlobalTitle")
} else {
title = a.Tr.SLocalize(strings.Title(current) + "Title")
}
content = fmt.Sprintf("</pre>\n\n## %s\n<pre>\n", title)
file.WriteString(content)
}
// workaround to include menu keybinding in cheatsheet
// could not add this Description field directly to keybindings.go,
// because then menu key would be displayed in menu itself and that is undesirable
if key == "x" {
binding.Description = a.Tr.SLocalize("menu")
}
content = fmt.Sprintf("\t<kbd>%s</kbd>%s %s\n", key, strings.TrimPrefix(utils.WithPadding(key, padWidth), key), binding.Description)
file.WriteString(content)
}
}
}