Compare commits

..
3 Commits
Author SHA1 Message Date
Jesse Duffield b241962aed bump version to v0.1.69 2018-08-18 17:16:41 +10:00
Jesse Duffield 7dff573f0c bump version to v0.1.68 2018-08-18 17:13:29 +10:00
Jesse Duffield 83c3de33ee leave bump commits out of release notes 2018-08-18 17:13:12 +10:00
961 changed files with 27223 additions and 196054 deletions
+18 -54
View File
@@ -1,62 +1,26 @@
# Golang CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-go/ for more details
version: 2
jobs:
build:
docker:
- image: circleci/golang:1.12
environment:
GO111MODULE: "on"
# specify the version
- image: circleci/golang:1.9
# Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images
# documented at https://circleci.com/docs/2.0/circleci-images/
# - image: circleci/postgres:9.4
#### TEMPLATE_NOTE: go expects specific checkout path representing url
#### expecting it in the form of
#### /go/src/github.com/circleci/go-tool
#### /go/src/bitbucket.org/circleci/go-tool
working_directory: /go/src/github.com/jesseduffield/lazygit
steps:
- checkout
- run:
name: Run gofmt -s
command: |
if [ $(find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;|wc -l) -gt 0 ]; then
find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;
exit 1;
fi
- restore_cache:
keys:
- pkg-cache-{{ checksum "go.sum" }}-v5
- run:
name: Run tests
command: |
./test.sh
- run:
name: Push on codecov result
command: |
bash <(curl -s https://codecov.io/bash)
- run:
name: Compile project on every platform
command: |
go get github.com/mitchellh/gox
GOFLAGS=-mod=vendor gox -parallel 10 -os "linux freebsd netbsd windows" -osarch "darwin/i386 darwin/amd64"
- save_cache:
key: pkg-cache-{{ checksum "go.sum" }}-v5
paths:
- ~/.cache/go-build
release:
docker:
- image: circleci/golang:1.12
working_directory: /go/src/github.com/jesseduffield/lazygit
steps:
- checkout
- run:
name: Run gorelease
command: |
curl -sL https://git.io/goreleaser | bash
workflows:
version: 2
build:
jobs:
- build
release:
jobs:
- release:
filters:
tags:
only: /v[0-9]+(\.[0-9]+)*/
branches:
ignore: /.*/
# specify any bash command here prefixed with `run: `
- run: go test -v ./...
- run: bash <(curl -s https://codecov.io/bash)
-4
View File
@@ -1,4 +0,0 @@
# These are supported funding model platforms
ko_fi: jesseduffield
custom: ['https://donorbox.org/lazygit']
-4
View File
@@ -1,9 +1,6 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
@@ -26,7 +23,6 @@ If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. Windows]
- Lazygit Version [e.g. v0.1.45]
- The last commit id if you built project from sources (run : ```git-rev parse HEAD```)
**Additional context**
Add any other context about the problem here.
-14
View File
@@ -1,14 +0,0 @@
---
name: Discussion
about: Begin a discussion
title: ''
labels: discussion
assignees: ''
---
**Topic**
A clear and concise description of what you want to discuss
**Your thoughts**
What you have to say about the topic
@@ -1,9 +1,6 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
+6 -27
View File
@@ -1,28 +1,7 @@
# Please do not add personal files
# Logs
*.log
# Hidden
.*
# TODO
TODO.*
# Notes
*.notes
# Tests
development.log
commands.log
extra/lgit.rb
notes/go.notes
TODO.notes
TODO.md
test/repos/repo
coverage.txt
# Binaries
lazygit
# Exceptions
!.gitignore
!.goreleaser.yml
!.circleci/
!.github/
test/git_server/data
+1 -2
View File
@@ -13,9 +13,8 @@ builds:
- arm
- arm64
- 386
# Default is `-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}`.
ldflags:
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.buildSource=binaryRelease
- -s -w -X main.version={{.Version}} -X main.tag={{.Tag}} -X main.date={{.Date}} -X main.commit={{.Commit}} -X "main.goos={{.Os}} -X "main.arch={{.Arch}} -X "main.arm={{.Arm}}"
archive:
replacements:
+3 -4
View File
@@ -15,10 +15,9 @@ welcome your pull requests:
1. Fork the repo and create your branch from `master`.
2. If you've added code that should be tested, add tests.
3. If you've added code that need documentation, update the documentation.
4. Make sure your code follows the [effective go](https://golang.org/doc/effective_go.html) guidelines as much as possible.
5. Be sure to test your modifications.
6. Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
7. Issue that pull request!
4. Be sure to test your modifications.
5. Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
6. Issue that pull request!
## Code of conduct
Please note by participating in this project, you agree to abide by the [code of conduct].
-15
View File
@@ -1,15 +0,0 @@
# run with:
# docker build -t lazygit .
# docker run -it lazygit:latest /bin/sh -l
FROM golang:alpine
WORKDIR /go/src/github.com/jesseduffield/lazygit/
COPY ./ .
RUN CGO_ENABLED=0 GOOS=linux go build
FROM alpine:latest
RUN apk add -U git xdg-utils
WORKDIR /go/src/github.com/jesseduffield/lazygit/
COPY --from=0 /go/src/github.com/jesseduffield/lazygit /go/src/github.com/jesseduffield/lazygit
COPY --from=0 /go/src/github.com/jesseduffield/lazygit/lazygit /bin/
RUN echo "alias gg=lazygit" >> ~/.profile
Generated
+450
View File
@@ -0,0 +1,450 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
digest = "1:b2339e83ce9b5c4f79405f949429a7f68a9a904fed903c672aac1e7ceb7f5f02"
name = "github.com/Sirupsen/logrus"
packages = ["."]
pruneopts = "NUT"
revision = "3e01752db0189b9157070a0e1668a620f9a85da2"
version = "v1.0.6"
[[projects]]
branch = "master"
digest = "1:cd7ba2b29e93e2a8384e813dfc80ebb0f85d9214762e6ca89bb55a58092eab87"
name = "github.com/cloudfoundry/jibber_jabber"
packages = ["."]
pruneopts = "NUT"
revision = "bcc4c8345a21301bf47c032ff42dd1aae2fe3027"
[[projects]]
digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39"
name = "github.com/davecgh/go-spew"
packages = ["spew"]
pruneopts = "NUT"
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
digest = "1:de4a74b504df31145ffa8ca0c4edbffa2f3eb7f466753962184611b618fa5981"
name = "github.com/emirpasic/gods"
packages = [
"containers",
"lists",
"lists/arraylist",
"trees",
"trees/binaryheap",
"utils",
]
pruneopts = "NUT"
revision = "f6c17b524822278a87e3b3bd809fec33b51f5b46"
version = "v1.9.0"
[[projects]]
digest = "1:ade392a843b2035effb4b4a2efa2c3bab3eb29b992e98bacf9c898b0ecb54e45"
name = "github.com/fatih/color"
packages = ["."]
pruneopts = "NUT"
revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4"
version = "v1.7.0"
[[projects]]
digest = "1:1b91ae0dc69a41d4c2ed23ea5cffb721ea63f5037ca4b81e6d6771fbb8f45129"
name = "github.com/fsnotify/fsnotify"
packages = ["."]
pruneopts = "NUT"
revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9"
version = "v1.4.7"
[[projects]]
branch = "master"
digest = "1:4a8ed9b8cf22bd03bee5d74179fa06a282e4a73b6de949f7a865ff56cd2537e0"
name = "github.com/golang-collections/collections"
packages = ["stack"]
pruneopts = "NUT"
revision = "604e922904d35e97f98a774db7881f049cd8d970"
[[projects]]
branch = "master"
digest = "1:11c6c696067d3127ecf332b10f89394d386d9083f82baf71f40f2da31841a009"
name = "github.com/hashicorp/hcl"
packages = [
".",
"hcl/ast",
"hcl/parser",
"hcl/printer",
"hcl/scanner",
"hcl/strconv",
"hcl/token",
"json/parser",
"json/scanner",
"json/token",
]
pruneopts = "NUT"
revision = "ef8a98b0bbce4a65b5aa4c368430a80ddc533168"
[[projects]]
branch = "master"
digest = "1:62fe3a7ea2050ecbd753a71889026f83d73329337ada66325cbafd5dea5f713d"
name = "github.com/jbenet/go-context"
packages = ["io"]
pruneopts = "NUT"
revision = "d14ea06fba99483203c19d92cfcd13ebe73135f4"
[[projects]]
branch = "master"
digest = "1:c9a848b0484a72da2dae28957b4f67501fe27fa38bc73f4713e454353c0a4a60"
name = "github.com/jesseduffield/gocui"
packages = ["."]
pruneopts = "NUT"
revision = "432b7f6215f81ef1aaa1b2d9b69887822923cf79"
[[projects]]
digest = "1:8021af4dcbd531ae89433c8c3a6520e51064114aaf8eb1724c3cf911c497c9ba"
name = "github.com/kevinburke/ssh_config"
packages = ["."]
pruneopts = "NUT"
revision = "9fc7bb800b555d63157c65a904c86a2cc7b4e795"
version = "0.4"
[[projects]]
digest = "1:d244f8666a838fe6ad70ec8fe77f50ebc29fdc3331a2729ba5886bef8435d10d"
name = "github.com/magiconair/properties"
packages = ["."]
pruneopts = "NUT"
revision = "c2353362d570a7bfa228149c62842019201cfb71"
version = "v1.8.0"
[[projects]]
digest = "1:08c231ec84231a7e23d67e4b58f975e1423695a32467a362ee55a803f9de8061"
name = "github.com/mattn/go-colorable"
packages = ["."]
pruneopts = "NUT"
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
version = "v0.0.9"
[[projects]]
digest = "1:bc4f7eec3b7be8c6cb1f0af6c1e3333d5bb71072951aaaae2f05067b0803f287"
name = "github.com/mattn/go-isatty"
packages = ["."]
pruneopts = "NUT"
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
version = "v0.0.3"
[[projects]]
digest = "1:cb591533458f6eb6e2c1065ff3eac6b50263d7847deb23fc9f79b25bc608970e"
name = "github.com/mattn/go-runewidth"
packages = ["."]
pruneopts = "NUT"
revision = "9e777a8366cce605130a531d2cd6363d07ad7317"
version = "v0.0.2"
[[projects]]
digest = "1:a25c9a6b41e100f4ce164db80260f2b687095ba9d8b46a1d6072d3686cc020db"
name = "github.com/mgutz/str"
packages = ["."]
pruneopts = "NUT"
revision = "968bf66e3da857419e4f6e71b2d5c9ae95682dc4"
version = "v1.2.0"
[[projects]]
branch = "master"
digest = "1:a4df73029d2c42fabcb6b41e327d2f87e685284ec03edf76921c267d9cfc9c23"
name = "github.com/mitchellh/go-homedir"
packages = ["."]
pruneopts = "NUT"
revision = "58046073cbffe2f25d425fe1331102f55cf719de"
[[projects]]
branch = "master"
digest = "1:5fe20cfe4ef484c237cec9f947b2a6fa90bad4b8610fd014f0e4211e13d82d5d"
name = "github.com/mitchellh/mapstructure"
packages = ["."]
pruneopts = "NUT"
revision = "f15292f7a699fcc1a38a80977f80a046874ba8ac"
[[projects]]
digest = "1:2c34c77bf3ec848da26e48af58fc511ed52750961fa848399d122882b8890928"
name = "github.com/nicksnyder/go-i18n"
packages = [
"v2/i18n",
"v2/internal",
"v2/internal/plural",
]
pruneopts = "NUT"
revision = "a16b91a3ba80db3a2301c70d1d302d42251c9079"
version = "v2.0.0-beta.5"
[[projects]]
branch = "master"
digest = "1:34d9354c2c5d916c05864327553047df59fc10e86ff1f408e4136eba0a25a5ec"
name = "github.com/nsf/termbox-go"
packages = ["."]
pruneopts = "NUT"
revision = "5c94acc5e6eb520f1bcd183974e01171cc4c23b3"
[[projects]]
digest = "1:cf254277d898b713195cc6b4a3fac8bf738b9f1121625df27843b52b267eec6c"
name = "github.com/pelletier/go-buffruneio"
packages = ["."]
pruneopts = "NUT"
revision = "c37440a7cf42ac63b919c752ca73a85067e05992"
version = "v0.2.0"
[[projects]]
digest = "1:51ea800cff51752ff68e12e04106f5887b4daec6f9356721238c28019f0b42db"
name = "github.com/pelletier/go-toml"
packages = ["."]
pruneopts = "NUT"
revision = "c01d1270ff3e442a8a57cddc1c92dc1138598194"
version = "v1.2.0"
[[projects]]
digest = "1:d917313f309bda80d27274d53985bc65651f81a5b66b820749ac7f8ef061fd04"
name = "github.com/sergi/go-diff"
packages = ["diffmatchpatch"]
pruneopts = "NUT"
revision = "1744e2970ca51c86172c8190fadad617561ed6e7"
version = "v1.0.0"
[[projects]]
branch = "master"
digest = "1:41618aee8828e62dfe62d44f579c06892d0e98907d1c6d5bcd83bfe8536ec5a3"
name = "github.com/shibukawa/configdir"
packages = ["."]
pruneopts = "NUT"
revision = "e180dbdc8da04c4fa04272e875ce64949f38bd3e"
[[projects]]
digest = "1:330e9062b308ac597e28485699c02223bd052437a6eed32a173c9227dcb9d95a"
name = "github.com/spf13/afero"
packages = [
".",
"mem",
]
pruneopts = "NUT"
revision = "787d034dfe70e44075ccc060d346146ef53270ad"
version = "v1.1.1"
[[projects]]
digest = "1:3fa7947ca83b98ae553590d993886e845a4bff19b7b007e869c6e0dd3b9da9cd"
name = "github.com/spf13/cast"
packages = ["."]
pruneopts = "NUT"
revision = "8965335b8c7107321228e3e3702cab9832751bac"
version = "v1.2.0"
[[projects]]
branch = "master"
digest = "1:f29f83301ed096daed24a90f4af591b7560cb14b9cc3e1827abbf04db7269ab5"
name = "github.com/spf13/jwalterweatherman"
packages = ["."]
pruneopts = "NUT"
revision = "14d3d4c518341bea657dd8a226f5121c0ff8c9f2"
[[projects]]
digest = "1:e3707aeaccd2adc89eba6c062fec72116fe1fc1ba71097da85b4d8ae1668a675"
name = "github.com/spf13/pflag"
packages = ["."]
pruneopts = "NUT"
revision = "9a97c102cda95a86cec2345a6f09f55a939babf5"
version = "v1.0.2"
[[projects]]
digest = "1:454979540e2a1582f375a17c106cf4e11e3bcac4baffb4af23e515c87f87de13"
name = "github.com/spf13/viper"
packages = ["."]
pruneopts = "NUT"
revision = "907c19d40d9a6c9bb55f040ff4ae45271a4754b9"
version = "v1.1.0"
[[projects]]
digest = "1:ccca1dcd18bc54e23b517a3c5babeff2e3924a7d8fc1932162225876cfe4bfb0"
name = "github.com/src-d/gcfg"
packages = [
".",
"scanner",
"token",
"types",
]
pruneopts = "NUT"
revision = "f187355171c936ac84a82793659ebb4936bc1c23"
version = "v1.3.0"
[[projects]]
digest = "1:cd5ffc5bda4e0296ab3e4de90dbb415259c78e45e7fab13694b14cde8ab74541"
name = "github.com/tcnksm/go-gitconfig"
packages = ["."]
pruneopts = "NUT"
revision = "d154598bacbf4501c095a309753c5d4af66caa81"
version = "v0.1.2"
[[projects]]
digest = "1:3148cb3478c26a92b4c1a18abb9428234b281e278af6267840721a24b6cbc6a3"
name = "github.com/xanzy/ssh-agent"
packages = ["."]
pruneopts = "NUT"
revision = "640f0ab560aeb89d523bb6ac322b1244d5c3796c"
version = "v0.2.0"
[[projects]]
branch = "master"
digest = "1:dfcb1b2db354cafa48fc3cdafe4905a08bec4a9757919ab07155db0ca23855b4"
name = "golang.org/x/crypto"
packages = [
"cast5",
"curve25519",
"ed25519",
"ed25519/internal/edwards25519",
"internal/chacha20",
"internal/subtle",
"openpgp",
"openpgp/armor",
"openpgp/elgamal",
"openpgp/errors",
"openpgp/packet",
"openpgp/s2k",
"poly1305",
"ssh",
"ssh/agent",
"ssh/knownhosts",
"ssh/terminal",
]
pruneopts = "NUT"
revision = "de0752318171da717af4ce24d0a2e8626afaeb11"
[[projects]]
branch = "master"
digest = "1:76ee51c3f468493aff39dbacc401e8831fbb765104cbf613b89bef01cf4bad70"
name = "golang.org/x/net"
packages = ["context"]
pruneopts = "NUT"
revision = "c39426892332e1bb5ec0a434a079bf82f5d30c54"
[[projects]]
branch = "master"
digest = "1:ec76a40fbfda0c329ee58f4e3b14b4279a939efce89eca020e934e2e5234eddd"
name = "golang.org/x/sys"
packages = [
"unix",
"windows",
]
pruneopts = "NUT"
revision = "98c5dad5d1a0e8a73845ecc8897d0bd56586511d"
[[projects]]
digest = "1:a95288ef1ef4dfad6cba7fe30843e1683f71bc28c912ca1ba3f6a539d44db739"
name = "golang.org/x/text"
packages = [
"internal/gen",
"internal/tag",
"internal/triegen",
"internal/ucd",
"language",
"transform",
"unicode/cldr",
"unicode/norm",
]
pruneopts = "NUT"
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
version = "v0.3.0"
[[projects]]
digest = "1:47a697b155f5214ff14e68e39ce9c2e8d93e1fb035ae5ba7e247d044e0ce64e3"
name = "gopkg.in/src-d/go-billy.v4"
packages = [
".",
"helper/chroot",
"helper/polyfill",
"osfs",
"util",
]
pruneopts = "NUT"
revision = "83cf655d40b15b427014d7875d10850f96edba14"
version = "v4.2.0"
[[projects]]
digest = "1:e66078da2bd6e53c72518d7f6ae0c3c8c7f34c0df12c39435ce34a6bce165525"
name = "gopkg.in/src-d/go-git.v4"
packages = [
".",
"config",
"internal/revision",
"plumbing",
"plumbing/cache",
"plumbing/filemode",
"plumbing/format/config",
"plumbing/format/diff",
"plumbing/format/gitignore",
"plumbing/format/idxfile",
"plumbing/format/index",
"plumbing/format/objfile",
"plumbing/format/packfile",
"plumbing/format/pktline",
"plumbing/object",
"plumbing/protocol/packp",
"plumbing/protocol/packp/capability",
"plumbing/protocol/packp/sideband",
"plumbing/revlist",
"plumbing/storer",
"plumbing/transport",
"plumbing/transport/client",
"plumbing/transport/file",
"plumbing/transport/git",
"plumbing/transport/http",
"plumbing/transport/internal/common",
"plumbing/transport/server",
"plumbing/transport/ssh",
"storage",
"storage/filesystem",
"storage/filesystem/dotgit",
"storage/memory",
"utils/binary",
"utils/diff",
"utils/ioutil",
"utils/merkletrie",
"utils/merkletrie/filesystem",
"utils/merkletrie/index",
"utils/merkletrie/internal/frame",
"utils/merkletrie/noder",
]
pruneopts = "NUT"
revision = "43d17e14b714665ab5bc2ecc220b6740779d733f"
[[projects]]
digest = "1:b233ad4ec87ac916e7bf5e678e98a2cb9e8b52f6de6ad3e11834fc7a71b8e3bf"
name = "gopkg.in/warnings.v0"
packages = ["."]
pruneopts = "NUT"
revision = "ec4a0fea49c7b46c2aeb0b51aac55779c607e52b"
version = "v0.1.2"
[[projects]]
digest = "1:7c95b35057a0ff2e19f707173cc1a947fa43a6eb5c4d300d196ece0334046082"
name = "gopkg.in/yaml.v2"
packages = ["."]
pruneopts = "NUT"
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
version = "v2.2.1"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = [
"github.com/Sirupsen/logrus",
"github.com/cloudfoundry/jibber_jabber",
"github.com/davecgh/go-spew/spew",
"github.com/fatih/color",
"github.com/golang-collections/collections/stack",
"github.com/jesseduffield/gocui",
"github.com/mgutz/str",
"github.com/nicksnyder/go-i18n/v2/i18n",
"github.com/shibukawa/configdir",
"github.com/spf13/viper",
"github.com/tcnksm/go-gitconfig",
"golang.org/x/text/language",
"gopkg.in/src-d/go-git.v4",
"gopkg.in/src-d/go-git.v4/plumbing",
]
solver-name = "gps-cdcl"
solver-version = 1
+42
View File
@@ -0,0 +1,42 @@
# Gopkg.toml example
#
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
[prune]
go-tests = true
unused-packages = true
non-go = true
[[constraint]]
name = "github.com/fatih/color"
version = "1.7.0"
[[constraint]]
branch = "master"
name = "github.com/golang-collections/collections"
[[constraint]]
branch = "master"
name = "github.com/jesseduffield/gocui"
[[constraint]]
name = "gopkg.in/src-d/go-git.v4"
revision = "43d17e14b714665ab5bc2ecc220b6740779d733f"
+36 -58
View File
@@ -1,31 +1,31 @@
# lazygit [![CircleCI](https://circleci.com/gh/jesseduffield/lazygit.svg?style=svg)](https://circleci.com/gh/jesseduffield/lazygit) [![codecov](https://codecov.io/gh/jesseduffield/lazygit/branch/master/graph/badge.svg)](https://codecov.io/gh/jesseduffield/lazygit) [![Go Report Card](https://goreportcard.com/badge/github.com/jesseduffield/lazygit)](https://goreportcard.com/report/github.com/jesseduffield/lazygit) [![GolangCI](https://golangci.com/badges/github.com/jesseduffield/lazygit.svg)](https://golangci.com) [![GoDoc](https://godoc.org/github.com/jesseduffield/lazygit?status.svg)](http://godoc.org/github.com/jesseduffield/lazygit) [![GitHub tag](https://img.shields.io/github/tag/jesseduffield/lazygit.svg)]()
# lazygit [![Go Report Card](https://goreportcard.com/badge/github.com/jesseduffield/lazygit)](https://goreportcard.com/report/github.com/jesseduffield/lazygit)
A simple terminal UI for git commands, written in Go with the [gocui](https://github.com/jroimartin/gocui 'gocui') library.
A simple terminal UI for git commands, written in Go with the [gocui](https://github.com/jroimartin/gocui "gocui") library.
Are YOU tired of typing every git command directly into the terminal, but you're
too stubborn to use Sourcetree because you'll never forgive Atlassian for making
Jira? This is the app for you!
![Gif](/docs/resources/lazygit-example.gif)
- [Installation](https://github.com/jesseduffield/lazygit#installation)
- [Usage](https://github.com/jesseduffield/lazygit#usage),
[Keybindings](/docs/keybindings)
- [Cool Features](https://github.com/jesseduffield/lazygit#cool-features)
- [Contributing](https://github.com/jesseduffield/lazygit#contributing)
- [Video Tutorial](https://youtu.be/VDXvbHZYeKY)
- [Twitch Stream](https://www.twitch.tv/jesseduffield)
![Gif](https://image.ibb.co/mmeXho/optimisedgif.gif)
* [Installation](https://github.com/jesseduffield/lazygit#installation)
* [Usage](https://github.com/jesseduffield/lazygit#usage),
[Keybindings](https://github.com/jesseduffield/lazygit/blob/master/docs/Keybindings.md)
* [Cool Features](https://github.com/jesseduffield/lazygit#cool-features)
* [Contributing](https://github.com/jesseduffield/lazygit#contributing)
* [Video Tutorial](https://www.youtube.com/watch?v=VDXvbHZYeKY)
* [Twitch Stream](https://www.twitch.tv/jesseduffield)
## Installation
### Homebrew
```sh
brew tap jesseduffield/lazygit
brew install lazygit
```
### Ubuntu
Packages for Ubuntu 16.04, 18.04 and 18.10 are available via [Launchpad PPA](https://launchpad.net/~lazygit-team).
**Release builds**
@@ -49,7 +49,6 @@ sudo apt-get install lazygit
```
### Void Linux
Packages for Void Linux are available in the distro repo
They follow upstream latest releases
@@ -59,97 +58,76 @@ sudo xbps-install -S lazygit
```
### Arch Linux
Packages for Arch Linux are available via AUR (Arch User Repository).
There are two packages. The stable one which is built with the latest release
and the git version which builds from the most recent commit.
- Stable: https://aur.archlinux.org/packages/lazygit/
- Development: https://aur.archlinux.org/packages/lazygit-git/
* Stable: https://aur.archlinux.org/packages/lazygit/
* Development: https://aur.archlinux.org/packages/lazygit-git/
Instruction of how to install AUR content can be found here:
https://wiki.archlinux.org/index.php/Arch_User_Repository
### Conda
Released versions are available for different platforms, see https://anaconda.org/conda-forge/lazygit
```sh
conda install -c conda-forge lazygit
```
### Binary Release (Windows/Linux/OSX)
You can download a binary release [here](https://github.com/jesseduffield/lazygit/releases).
### Go
```sh
go get github.com/jesseduffield/lazygit
```
Please note:
If you get an error claiming that lazygit cannot be found or is not defined, you
may need to add `~/go/bin` to your \$PATH (MacOS/Linux), or `%HOME%\go\bin`
may need to add `~/go/bin` to your $PATH (MacOS/Linux), or `%HOME%\go\bin`
(Windows). Not to be mistaked for `C:\Go\bin` (which is for Go's own binaries,
not apps like Lazygit).
## Usage
Call `lazygit` in your terminal inside a git repository. If you want, you can
also add an alias for this with `echo "alias lg='lazygit'" >> ~/.zshrc` (or
whichever rc file you're using).
- Basic video tutorial [here](https://youtu.be/VDXvbHZYeKY).
- List of keybindings
[here](/docs/keybindings).
* Basic video tutorial [here](https://www.youtube.com/watch?v=VDXvbHZYeKY).
* List of keybindings
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/Keybindings.md).
## Cool features
- Adding files easily
- Resolving merge conflicts
- Easily check out recent branches
- Scroll through logs/diffs of branches/commits/stash
- Quick pushing/pulling
- Squash down and rename commits
* Adding files easily
* Resolving merge conflicts
* Easily check out recent branches
* Scroll through logs/diffs of branches/commits/stash
* Quick pushing/pulling
* Squash down and rename commits
### Resolving merge conflicts
![Gif](https://image.ibb.co/iyxUTT/shortermerging.gif)
![Gif](/docs/resources/resolving-merge-conflicts.gif)
### Viewing commit diffs
![Viewing Commit Diffs](https://image.ibb.co/gPD02o/capture.png)
### Interactive Rebasing
![Interactive Rebasing](/docs/resources/interactive-rebase.png)
## Milestones
- [x] Easy Installation (homebrew, release binaries)
- [ ] Configurable Keybindings
- [ ] Configurable Color Themes
- [ ] Spawning Subprocesses (help needed - have a look at https://github.com/jesseduffield/lazygit/pull/18)
- [ ] Maintainability
- [ ] Performance
- [ ] i18n
## Contributing
We love your input! Please check out the [contributing guide](CONTRIBUTING.md).
For contributor discussion about things not better discussed here in the repo, join the slack channel
[![Slack](/docs/resources/slack_rgb.png)](https://join.slack.com/t/lazygit/shared_invite/enQtNDE3MjIwNTYyMDA0LTM3Yjk3NzdiYzhhNTA1YjM4Y2M4MWNmNDBkOTI0YTE4YjQ1ZmI2YWRhZTgwNjg2YzhhYjg3NDBlMmQyMTI5N2M)
## Donate
If you would like to support the development of lazygit, please donate
[![Donate](https://d1iczxrky3cnb2.cloudfront.net/button-medium-blue.png)](https://donorbox.org/lazygit)
[![Slack](/files/slack_rgb.png)](https://join.slack.com/t/lazygit/shared_invite/enQtNDE3MjIwNTYyMDA0LTM3Yjk3NzdiYzhhNTA1YjM4Y2M4MWNmNDBkOTI0YTE4YjQ1ZmI2YWRhZTgwNjg2YzhhYjg3NDBlMmQyMTI5N2M)
## Work in progress
This is still a work in progress so there's still bugs to iron out and as this
is my first project in Go the code could no doubt use an increase in quality,
but I'll be improving on it whenever I find the time. If you have any feedback
feel free to [raise an issue](https://github.com/jesseduffield/lazygit/issues)/[submit a PR](https://github.com/jesseduffield/lazygit/pulls).
## Social
If you want to see what I (Jesse) am up to in terms of development, follow me on
[twitter](https://twitter.com/DuffieldJesse) or watch me program on
[twitch](https://www.twitch.tv/jesseduffield).
## Alternatives
If you find that lazygit doesn't quite satisfy your requirements, these may be a better fit:
- [tig](https://github.com/jonas/tig)
+1
View File
@@ -0,0 +1 @@
v0.1.69
+3 -65
View File
@@ -2,13 +2,11 @@
## Default:
```yaml
```
gui:
# stuff relating to the UI
scrollHeight: 2 # how many lines you scroll by
scrollPastBottom: true # enable scrolling past the bottom
theme:
lightTheme: false # For terminals with a light background
activeBorderColor:
- white
- bold
@@ -16,51 +14,10 @@
- white
optionsTextColor:
- blue
commitLength:
show: true
git:
merging:
# only applicable to unix users
manualCommit: false
skipHookPrefix: WIP
autoFetch: true
update:
method: prompt # can be: prompt | background | never
days: 14 # how often an update is checked for
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
confirmOnQuit: false
```
## Platform Defaults:
### Windows:
```yaml
# stuff relating to git
os:
openCommand: 'cmd /c "start "" {{filename}}"'
```
### Linux:
```yaml
os:
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'
```
### OSX:
```yaml
os:
openCommand: 'open {{filename}}'
```
### Recommended Config Values:
for users of VSCode
```yaml
os:
openCommand: 'code -r {{filename}}'
# stuff relating to the OS
```
## Color Attributes:
@@ -80,22 +37,3 @@ The available attributes are:
- bold
- reverse # useful for high-contrast
- underline
## Light terminal theme:
If you have issues with a light terminal theme where you can't read / see the text add these settings
```yaml
gui:
theme:
lightTheme: true
activeBorderColor:
- black
- bold
inactiveBorderColor:
- black
```
## Example Coloring:
![border example](/docs/resources/colored-border-example.png)
+81
View File
@@ -0,0 +1,81 @@
# Keybindings:
## Global:
<pre>
<kbd>←</kbd><kbd>→</kbd><kbd>↑</kbd><kbd>↓</kbd>/<kbd>h</kbd><kbd>j</kbd><kbd>k</kbd><kbd>l</kbd>: navigate
<kbd>PgUp</kbd>/<kbd>PgDn</kbd> or <kbd>ctrl</kbd>+<kbd>u</kbd>/<kbd>ctrl</kbd>+<kbd>d</kbd>: scroll diff panel
(for <kbd>PgUp</kbd> and <kbd>PgDn</kbd>, use <kbd>fn</kbd>+<kbd>up</kbd>/<kbd>fn</kbd>+<kbd>down</kbd> on osx)
<kbd>q</kbd>: quit
<kbd>p</kbd>: pull
<kbd>shift</kbd>+<kbd>P</kbd>: push
</pre>
## Status Panel:
<pre>
<kbd>e</kbd>: edit config file
<kbd>o</kbd>: open config file
</pre>
## Files Panel:
<pre>
<kbd>space</kbd>: toggle staged
<kbd>c</kbd>: commit changes
<kbd>shift</kbd>+<kbd>C</kbd>: commit using git editor
<kbd>shift</kbd>+<kbd>S</kbd>: stash files
<kbd>t</kbd>: add patched (i.e. pick chunks of a file to add)
<kbd>o</kbd>: open
<kbd>e</kbd>: edit
<kbd>s</kbd>: open in sublime (requires 'subl' command)
<kbd>v</kbd>: open in vscode (requires 'code' command)
<kbd>i</kbd>: add to .gitignore
<kbd>d</kbd>: delete if untracked checkout if tracked (aka go away)
<kbd>shift</kbd>+<kbd>R</kbd>: refresh files
</pre>
## Branches Panel:
<pre>
<kbd>space</kbd>: checkout branch
<kbd>f</kbd>: force checkout branch
<kbd>m</kbd>: merge into currently checked out branch
<kbd>c</kbd>: checkout by name
<kbd>n</kbd>: new branch
<kbd>d</kbd>: delete branch
</pre>
## Commits Panel:
<pre>
<kbd>s</kbd>: squash down (only available for topmost commit)
<kbd>r</kbd>: rename commit
<kbd>g</kbd>: reset to this commit
</pre>
## Stash Panel:
<pre>
<kbd>space</kbd>: apply
<kbd>g</kbd>: pop
<kbd>d</kbd>: drop
</pre>
## Popup Panel:
<pre>
<kbd>esc</kbd>: close/cancel
<kbd>enter</kbd>: confirm
<kbd>tab</kbd>: enter newline (if editing)
</pre>
## Resolving Merge Conflicts (Diff Panel):
<pre>
<kbd>←</kbd><kbd>→</kbd>/<kbd>h</kbd><kbd>l</kbd>: navigate conflicts
<kbd>↑</kbd><kbd>↓</kbd>/<kbd>k</kbd><kbd>j</kbd>: select hunk
<kbd>space</kbd>: pick hunk
<kbd>b</kbd>: pick both hunks
<kbd>z</kbd>: undo (only available while still inside diff panel)
</pre>
-128
View File
@@ -1,128 +0,0 @@
# Lazygit menu
## Global
<pre>
<kbd>m</kbd>: view merge/rebase options
<kbd>P</kbd>: push
<kbd>p</kbd>: pull
<kbd>R</kbd>: refresh
</pre>
## Status
<pre>
<kbd>e</kbd>: edit config file
<kbd>o</kbd>: open config file
<kbd>u</kbd>: check for update
<kbd>s</kbd>: switch to a recent repo
</pre>
## Files
<pre>
<kbd>c</kbd>: commit changes
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>A</kbd>: amend last commit
<kbd>C</kbd>: commit changes using git editor
<kbd>space</kbd>: toggle staged
<kbd>d</kbd>: view 'discard changes' options
<kbd>e</kbd>: edit file
<kbd>o</kbd>: open file
<kbd>i</kbd>: add to .gitignore
<kbd>r</kbd>: refresh files
<kbd>S</kbd>: stash files
<kbd>a</kbd>: stage/unstage all
<kbd>t</kbd>: add patch
<kbd>D</kbd>: view reset options
<kbd>enter</kbd>: stage individual hunks/lines
<kbd>f</kbd>: fetch
<kbd>X</kbd>: execute custom command
</pre>
## Branches
<pre>
<kbd>space</kbd>: checkout
<kbd>o</kbd>: create pull request
<kbd>c</kbd>: checkout by name
<kbd>F</kbd>: force checkout
<kbd>n</kbd>: new branch
<kbd>d</kbd>: delete branch
<kbd>r</kbd>: rebase branch
<kbd>M</kbd>: merge into currently checked out branch
<kbd>f</kbd>: fast-forward this branch from its upstream
</pre>
## Commits
<pre>
<kbd>s</kbd>: squash down
<kbd>r</kbd>: reword commit
<kbd>R</kbd>: rename commit with editor
<kbd>g</kbd>: reset to this commit
<kbd>f</kbd>: fixup commit
<kbd>F</kbd>: create fixup commit for this commit
<kbd>S</kbd>: squash above commits
<kbd>d</kbd>: delete commit
<kbd>ctrl+j</kbd>: move commit down one
<kbd>ctrl+k</kbd>: move commit up one
<kbd>e</kbd>: edit commit
<kbd>A</kbd>: amend commit with staged changes
<kbd>p</kbd>: pick commit (when mid-rebase)
<kbd>t</kbd>: revert commit
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>v</kbd>: paste commits (cherry-pick)
<kbd>enter</kbd>: view commit's files
<kbd>space</kbd>: select commit to diff with another commit
</pre>
## Stash
<pre>
<kbd>space</kbd>: apply
<kbd>g</kbd>: pop
<kbd>d</kbd>: drop
</pre>
## Commit files
<pre>
<kbd>esc</kbd>: go back
<kbd>c</kbd>: checkout file
<kbd>d</kbd>: discard this commit's changes to this file
<kbd>o</kbd>: open file
</pre>
## Main (Normal)
<pre>
<kbd>PgDn</kbd>: scroll down (fn+up)
<kbd>PgUp</kbd>: scroll up (fn+down)
</pre>
## Main (Staging)
<pre>
<kbd>esc</kbd>: return to files panel
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>space</kbd>: stage line
<kbd>a</kbd>: stage hunk
</pre>
## Main (Merging)
<pre>
<kbd>esc</kbd>: return to files panel
<kbd>space</kbd>: pick hunk
<kbd>b</kbd>: pick both hunks
<kbd>◄</kbd>: select previous conflict
<kbd>►</kbd>: select next conflict
<kbd>▲</kbd>: select top hunk
<kbd>▼</kbd>: select bottom hunk
<kbd>z</kbd>: undo
</pre>
-128
View File
@@ -1,128 +0,0 @@
# Lazygit menu
## Global
<pre>
<kbd>m</kbd>: bekijk merge/rebase opties
<kbd>P</kbd>: push
<kbd>p</kbd>: pull
<kbd>R</kbd>: verversen
</pre>
## Status
<pre>
<kbd>e</kbd>: verander config file
<kbd>o</kbd>: open config file
<kbd>u</kbd>: check voor updates
<kbd>s</kbd>: wissel naar een recente repo
</pre>
## Bestanden
<pre>
<kbd>c</kbd>: Commit veranderingen
<kbd>w</kbd>: commit veranderingen zonder pre-commit hook
<kbd>A</kbd>: wijzig laatste commit
<kbd>C</kbd>: commit veranderingen met de git editor
<kbd>space</kbd>: toggle staged
<kbd>d</kbd>: bekijk 'veranderingen ongedaan maken' opties
<kbd>e</kbd>: verander bestand
<kbd>o</kbd>: open bestand
<kbd>i</kbd>: voeg toe aan .gitignore
<kbd>r</kbd>: refresh bestanden
<kbd>S</kbd>: stash-bestanden
<kbd>a</kbd>: toggle staged alle
<kbd>t</kbd>: bewerkingen toevoegen
<kbd>D</kbd>: bekijk reset opties
<kbd>enter</kbd>: stage individuele hunks/lijnen
<kbd>f</kbd>: fetch
<kbd>X</kbd>: voor aangepast commando uit
</pre>
## Branches
<pre>
<kbd>space</kbd>: uitchecken
<kbd>o</kbd>: maak een pull-aanvraag
<kbd>c</kbd>: uitchecken bij naam
<kbd>F</kbd>: forceer checkout
<kbd>n</kbd>: nieuwe branch
<kbd>d</kbd>: verwijder branch
<kbd>r</kbd>: rebase branch
<kbd>M</kbd>: merge in met huidige checked out branch
<kbd>f</kbd>: fast-forward this branch from its upstream
</pre>
## Commits
<pre>
<kbd>s</kbd>: squash beneden
<kbd>r</kbd>: hernoem commit
<kbd>R</kbd>: rename commit with editor
<kbd>g</kbd>: reset naar deze commit
<kbd>f</kbd>: Fixup commit
<kbd>F</kbd>: creëer fixup commit voor deze commit
<kbd>S</kbd>: squash bovenstaande commits
<kbd>d</kbd>: verwijder commit
<kbd>ctrl+j</kbd>: verplaats commit 1 omlaag
<kbd>ctrl+k</kbd>: verplaats commit 1 omhoog
<kbd>e</kbd>: verander commit
<kbd>A</kbd>: wijzig commit met staged veranderingen
<kbd>p</kbd>: pick commit (when mid-rebase)
<kbd>t</kbd>: commit omgedaan maken
<kbd>c</kbd>: kopiëer commit (cherry-pick)
<kbd>C</kbd>: kopiëer commit reeks (cherry-pick)
<kbd>v</kbd>: plak commits (cherry-pick)
<kbd>enter</kbd>: bekijk gecommite bestanden
<kbd>space</kbd>: select commit to diff with another commit
</pre>
## Stash
<pre>
<kbd>space</kbd>: toepassen
<kbd>g</kbd>: pop
<kbd>d</kbd>: drop
</pre>
## Commit bestanden
<pre>
<kbd>esc</kbd>: ga terug
<kbd>c</kbd>: bestand uitchecken
<kbd>d</kbd>: uitsluit deze commit zijn veranderingen aan dit bestand
<kbd>o</kbd>: open bestand
</pre>
## Hoofd (Stage Lines/Hunks)
<pre>
<kbd>esc</kbd>: ga terug naar het bestanden paneel
<kbd>▲</kbd>: selecteer de vorige lijn
<kbd>▼</kbd>: selecteer de volgende lijn
<kbd>◄</kbd>: selecteer de vorige hunk
<kbd>►</kbd>: selecteer de volgende hunk
<kbd>space</kbd>: stage lijn
<kbd>a</kbd>: stage hunk
</pre>
## Hoofd (Merging)
<pre>
<kbd>esc</kbd>: ga terug naar het bestanden paneel
<kbd>space</kbd>: pick hunk
<kbd>b</kbd>: pick beide hunks
<kbd>◄</kbd>: selecteer voorgaand conflict
<kbd>►</kbd>: selecteer volgende conflict
<kbd>▲</kbd>: selecteer bovenste hunk
<kbd>▼</kbd>: selecteer onderste hunk
<kbd>z</kbd>: ongedaan maken
</pre>
## Hoofd (Normaal)
<pre>
<kbd>PgDn</kbd>: scroll omlaag (fn+up)
<kbd>PgUp</kbd>: scroll omhoog (fn+down)
</pre>
-128
View File
@@ -1,128 +0,0 @@
# Lazygit menu
## Globalne
<pre>
<kbd>m</kbd>: view merge/rebase options
<kbd>P</kbd>: push
<kbd>p</kbd>: pull
<kbd>R</kbd>: odśwież
</pre>
## Status
<pre>
<kbd>e</kbd>: edytuj plik konfiguracyjny
<kbd>o</kbd>: otwórz plik konfiguracyjny
<kbd>u</kbd>: sprawdź aktualizacje
<kbd>s</kbd>: switch to a recent repo
</pre>
## Pliki
<pre>
<kbd>c</kbd>: commituj zmiany
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>A</kbd>: zmień ostatnie zatwierdzenie
<kbd>C</kbd>: commituj zmiany używając edytora z gita
<kbd>space</kbd>: przełącz zatwierdzenie
<kbd>d</kbd>: view 'discard changes' options
<kbd>e</kbd>: edytuj plik
<kbd>o</kbd>: otwórz plik
<kbd>i</kbd>: dodaj do .gitignore
<kbd>r</kbd>: odśwież pliki
<kbd>S</kbd>: przechowaj pliki
<kbd>a</kbd>: przełącz wszystkie zatwierdzenia
<kbd>t</kbd>: dodaj łatkę
<kbd>D</kbd>: view reset options
<kbd>enter</kbd>: zatwierdź pojedyncze linie
<kbd>f</kbd>: fetch
<kbd>X</kbd>: execute custom command
</pre>
## Gałęzie
<pre>
<kbd>space</kbd>: przełącz
<kbd>o</kbd>: utwórz żądanie wyciągnięcia
<kbd>c</kbd>: przełącz używając nazwy
<kbd>F</kbd>: wymuś przełączenie
<kbd>n</kbd>: nowa gałąź
<kbd>d</kbd>: usuń gałąź
<kbd>r</kbd>: rebase branch
<kbd>M</kbd>: scal do obecnej gałęzi
<kbd>f</kbd>: fast-forward this branch from its upstream
</pre>
## Commity
<pre>
<kbd>s</kbd>: ściśnij w dół
<kbd>r</kbd>: przemianuj commit
<kbd>R</kbd>: przemianuj commit w edytorze
<kbd>g</kbd>: zresetuj do tego commita
<kbd>f</kbd>: napraw commit
<kbd>F</kbd>: create fixup commit for this commit
<kbd>S</kbd>: squash above commits
<kbd>d</kbd>: delete commit
<kbd>ctrl+j</kbd>: move commit down one
<kbd>ctrl+k</kbd>: move commit up one
<kbd>e</kbd>: edit commit
<kbd>A</kbd>: amend commit with staged changes
<kbd>p</kbd>: pick commit (when mid-rebase)
<kbd>t</kbd>: revert commit
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>v</kbd>: paste commits (cherry-pick)
<kbd>enter</kbd>: view commit's files
<kbd>space</kbd>: select commit to diff with another commit
</pre>
## Schowek
<pre>
<kbd>space</kbd>: zastosuj
<kbd>g</kbd>: wyciągnij
<kbd>d</kbd>: porzuć
</pre>
## Commit files
<pre>
<kbd>esc</kbd>: go back
<kbd>c</kbd>: checkout file
<kbd>d</kbd>: discard this commit's changes to this file
<kbd>o</kbd>: otwórz plik
</pre>
## Main (Normal)
<pre>
<kbd>PgDn</kbd>: scroll down (fn+up)
<kbd>PgUp</kbd>: scroll up (fn+down)
</pre>
## Main (Zatwierdzanie)
<pre>
<kbd>esc</kbd>: wróć do panelu plików
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>space</kbd>: zatwierdź linię
<kbd>a</kbd>: zatwierdź kawałek
</pre>
## Main (Merging)
<pre>
<kbd>esc</kbd>: wróć do panelu plików
<kbd>space</kbd>: pick hunk
<kbd>b</kbd>: pick both hunks
<kbd>◄</kbd>: select previous conflict
<kbd>►</kbd>: select next conflict
<kbd>▲</kbd>: select top hunk
<kbd>▼</kbd>: select bottom hunk
<kbd>z</kbd>: undo
</pre>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

-69
View File
@@ -1,69 +0,0 @@
module github.com/jesseduffield/lazygit
go 1.12
require (
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 // indirect
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect
github.com/aws/aws-sdk-go v1.15.21 // indirect
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
github.com/emirpasic/gods v1.9.0 // indirect
github.com/fatih/color v1.7.0
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/gliderlabs/ssh v0.2.2 // indirect
github.com/go-errors/errors v1.0.1
github.com/go-ini/ini v1.38.2 // indirect
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
github.com/google/go-cmp v0.3.1 // indirect
github.com/hashicorp/go-cleanhttp v0.0.0-20171218145408-d5fe4b57a186 // indirect
github.com/hashicorp/go-getter v0.0.0-20180809191950-4bda8fa99001 // indirect
github.com/hashicorp/go-safetemp v0.0.0-20180326211150-b1a1dbde6fdc // indirect
github.com/hashicorp/go-version v1.0.0 // indirect
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce // indirect
github.com/integrii/flaggy v1.2.2
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jesseduffield/go-getter v0.0.0-20180822080847-906e15686e63
github.com/jesseduffield/gocui v0.3.1-0.20190908012510-092b2290ee54
github.com/jesseduffield/pty v0.0.0-20181218102224-02db52c7e406
github.com/jesseduffield/rollrus v0.0.0-20190701125922-dd028cb0bfd7
github.com/jesseduffield/termbox-go v0.0.0-20180919093808-1e272ff78dcb // indirect
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1
github.com/kevinburke/ssh_config v0.0.0-20180317175531-9fc7bb800b55 // indirect
github.com/magiconair/properties v1.8.0 // indirect
github.com/mattn/go-colorable v0.0.9 // indirect
github.com/mattn/go-isatty v0.0.3 // indirect
github.com/mattn/go-runewidth v0.0.2 // indirect
github.com/mgutz/str v1.2.0
github.com/mitchellh/go-homedir v0.0.0-20180801233206-58046073cbff // indirect
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 // indirect
github.com/mitchellh/mapstructure v0.0.0-20180715050151-f15292f7a699 // indirect
github.com/nicksnyder/go-i18n/v2 v2.0.2
github.com/onsi/ginkgo v1.9.0 // indirect
github.com/onsi/gomega v1.6.0 // indirect
github.com/pelletier/go-buffruneio v0.2.0 // indirect
github.com/pelletier/go-toml v1.2.0 // indirect
github.com/sergi/go-diff v1.0.0 // indirect
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0
github.com/sirupsen/logrus v1.3.0
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 // indirect
github.com/spf13/afero v1.1.1 // indirect
github.com/spf13/cast v1.2.0 // indirect
github.com/spf13/jwalterweatherman v0.0.0-20180814060501-14d3d4c51834 // indirect
github.com/spf13/pflag v1.0.2 // indirect
github.com/spf13/viper v1.1.0
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
github.com/src-d/gcfg v1.3.0 // indirect
github.com/stretchr/testify v1.2.2
github.com/tcnksm/go-gitconfig v0.1.2
github.com/ulikunitz/xz v0.5.4 // indirect
github.com/xanzy/ssh-agent v0.2.0 // indirect
golang.org/x/text v0.3.2
gopkg.in/ini.v1 v1.46.0 // indirect
gopkg.in/src-d/go-billy.v4 v4.2.0 // indirect
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 // indirect
gopkg.in/src-d/go-git.v4 v4.0.0-20180807092216-43d17e14b714
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.2.1
)
-182
View File
@@ -1,182 +0,0 @@
github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/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.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-ini/ini v1.38.2 h1:6Hl/z3p3iFkA0dlDfzYxuFuUGD+kaweypF6btsR2/Q4=
github.com/go-ini/ini v1.38.2/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/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.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/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:qC+3MHkvfCXb1cA9YDpWZ7np8tPOXZceLrW+xyqOgmk=
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/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/integrii/flaggy v1.2.2 h1:SzL5kyEaW+Cb3RLxGG1ch9FFDLQPB6QuMdYoNu5JIo0=
github.com/integrii/flaggy v1.2.2/go.mod h1:tnTxHeTJbah0gQ6/K0RW0J7fMUBk9MCF5blhm43LNpI=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jesseduffield/go-getter v0.0.0-20180822080847-906e15686e63 h1:tbm85YuPi3d1LFAUr6yZyzZ4vR96ygV98rezZZ+ywbg=
github.com/jesseduffield/go-getter v0.0.0-20180822080847-906e15686e63/go.mod h1:fNqjRf+4XnTo2PrGN1JRb79b/BeoHwP4lU00f39SQY0=
github.com/jesseduffield/gocui v0.3.1-0.20190908012510-092b2290ee54 h1:zK8KCB55hNdBadw5iJll46xQL6WXEwEXbt9B+QyYswo=
github.com/jesseduffield/gocui v0.3.1-0.20190908012510-092b2290ee54/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/pty v0.0.0-20181218102224-02db52c7e406 h1:iYMH6h6SuWuBkIzRtymosE8NpSgTK0oRMfyTdVWgxzc=
github.com/jesseduffield/pty v0.0.0-20181218102224-02db52c7e406/go.mod h1:7jlS40+UhOqkZJDIG1B/H21xnuET/+fvbbnHCa8wSIo=
github.com/jesseduffield/roll v0.0.0-20190629104057-695be2e62b00 h1:+JaOkfBNYQYlGD7dgru8mCwYNEc5tRRI8mThlVANhSM=
github.com/jesseduffield/roll v0.0.0-20190629104057-695be2e62b00/go.mod h1:cWNQljQAWYBp4wchyGfql4q2jRNZXxiE1KhVQgz+JaM=
github.com/jesseduffield/rollrus v0.0.0-20190701125922-dd028cb0bfd7 h1:CRD7bVjlGIiV+M0jlsa+XWpneW0KY0e7Y4z3GWb5S4o=
github.com/jesseduffield/rollrus v0.0.0-20190701125922-dd028cb0bfd7/go.mod h1:VspA3aTkEo0Q7TPCLmX1uHNP+Wb4iSDX09hmTRo1QYc=
github.com/jesseduffield/termbox-go v0.0.0-20180919093808-1e272ff78dcb h1:cFHYEWpQEfzFZVKiKZytCUX4UwQixKSw0kd3WhluPsY=
github.com/jesseduffield/termbox-go v0.0.0-20180919093808-1e272ff78dcb/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4=
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 h1:12VvqtR6Aowv3l/EQUlocDHW2Cp4G9WJVH7uyH8QFJE=
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+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/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/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/v2 v2.0.2 h1:KsHGcTByIM0mHZKQGy0nlJLOjPNjQ6MVib/3PvsBDNY=
github.com/nicksnyder/go-i18n/v2 v2.0.2/go.mod h1:JXS4+OKhbcwDoVTEj0sLFWL1vOwec2g/YBAxZ9owJqY=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.9.0 h1:SZjF721BByVj8QH636/8S2DnX4n0Re3SteMmw3N+tzc=
github.com/onsi/ginkgo v1.9.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.6.0 h1:8XTW0fcJZEq9q+Upcyws4JSGua2MFysCL5xkaSgHc+M=
github.com/onsi/gomega v1.6.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
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.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/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.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME=
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8=
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
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/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
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-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284 h1:rlLehGeYg6jfoyz/eDqDU1iRXLKfR42nnNh57ytKEWo=
golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b h1:ag/x1USPSsqHud38I9BAC88qdNLDHHtQ4mlgQIZPPNA=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
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/ini.v1 v1.46.0 h1:VeDZbLYGaupuvIrsYCEOe/L/2Pcs5n7hdO1ZTjporag=
gopkg.in/ini.v1 v1.46.0/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.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg=
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
gopkg.in/src-d/go-git.v4 v4.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=
+37 -62
View File
@@ -1,23 +1,26 @@
package main
import (
"flag"
"fmt"
"log"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"github.com/go-errors/errors"
"github.com/integrii/flaggy"
"github.com/jesseduffield/lazygit/pkg/app"
"github.com/jesseduffield/lazygit/pkg/config"
)
var (
commit string
version = "unversioned"
date string
buildSource = "unknown"
commit string
version = "unversioned"
date string
goos string
arch string
arm string
debuggingFlag = flag.Bool("debug", false, "a boolean")
versionFlag = flag.Bool("v", false, "Print the current version")
)
func projectPath(path string) string {
@@ -25,62 +28,34 @@ func projectPath(path string) string {
return filepath.FromSlash(gopath + "/src/github.com/jesseduffield/lazygit/" + path)
}
func main() {
flaggy.DefaultParser.ShowVersionWithVersionFlag = false
repoPath := "."
flaggy.String(&repoPath, "p", "path", "Path of git repo")
dump := ""
flaggy.AddPositionalValue(&dump, "gitargs", 1, false, "Todo file")
flaggy.DefaultParser.PositionalFlags[0].Hidden = true
versionFlag := false
flaggy.Bool(&versionFlag, "v", "version", "Print the current version")
debuggingFlag := false
flaggy.Bool(&debuggingFlag, "d", "debug", "Run in debug mode with logging")
configFlag := false
flaggy.Bool(&configFlag, "c", "config", "Print the current default config")
flaggy.Parse()
if versionFlag {
fmt.Printf("commit=%s, build date=%s, build source=%s, version=%s, os=%s, arch=%s\n", commit, date, buildSource, version, runtime.GOOS, runtime.GOARCH)
os.Exit(0)
}
if configFlag {
fmt.Printf("%s\n", config.GetDefaultConfig())
os.Exit(0)
}
if repoPath != "." {
if err := os.Chdir(repoPath); err != nil {
log.Fatal(err.Error())
}
}
appConfig, err := config.NewAppConfig("lazygit", version, commit, date, buildSource, debuggingFlag)
// when building the binary, `version` is set as a compile-time variable, along
// with `date` and `commit`. If this program has been opened directly via go,
// we will populate the `version` with VERSION in the lazygit root directory
func fallbackVersion() string {
path := projectPath("VERSION")
byteVersion, err := ioutil.ReadFile(path)
if err != nil {
log.Fatal(err.Error())
return "unversioned"
}
return string(byteVersion)
}
func main() {
flag.Parse()
if version == "unversioned" {
version = fallbackVersion()
}
if *versionFlag {
fmt.Printf("commit=%s, build date=%s, version=%s, os=%s, arch=%s, arm=%s\n", commit, date, version, goos, arch, arm)
os.Exit(0)
}
appConfig, err := config.NewAppConfig("lazygit", version, commit, date, debuggingFlag)
if err != nil {
panic(err)
}
app, err := app.NewApp(appConfig)
if err == nil {
err = app.Run()
}
if err != nil {
if errorMessage, known := app.KnownError(err); known {
log.Fatal(errorMessage)
}
newErr := errors.Wrap(err, 0)
stackTrace := newErr.ErrorStack()
app.Log.Error(stackTrace)
log.Fatal(fmt.Sprintf("%s\n\n%s", app.Tr.SLocalize("ErrorOccurred"), stackTrace))
}
app.Log.Info(err)
app.GitCommand.SetupGit()
app.Gui.RunWithSubprocesses()
}
+22 -162
View File
@@ -1,69 +1,36 @@
package app
import (
"bufio"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/Sirupsen/logrus"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/updates"
"github.com/jesseduffield/rollrus"
"github.com/shibukawa/configdir"
"github.com/sirupsen/logrus"
)
// App struct
type App struct {
closers []io.Closer
Config config.AppConfigurer
Log *logrus.Entry
OSCommand *commands.OSCommand
GitCommand *commands.GitCommand
Gui *gui.Gui
Tr *i18n.Localizer
Updater *updates.Updater // may only need this on the Gui
ClientContext string
Config config.AppConfigurer
Log *logrus.Logger
OSCommand *commands.OSCommand
GitCommand *commands.GitCommand
Gui *gui.Gui
Tr *i18n.Localizer
}
type errorMapping struct {
originalError string
newError string
}
func newProductionLogger(config config.AppConfigurer) *logrus.Logger {
func newLogger(config config.AppConfigurer) *logrus.Logger {
log := logrus.New()
log.Out = ioutil.Discard
log.SetLevel(logrus.ErrorLevel)
return log
}
func globalConfigDir() string {
configDirs := configdir.New("jesseduffield", "lazygit")
configDir := configDirs.QueryFolders(configdir.Global)[0]
return configDir.Path
}
func getLogLevel() logrus.Level {
strLevel := os.Getenv("LOG_LEVEL")
level, err := logrus.ParseLevel(strLevel)
if err != nil {
return logrus.DebugLevel
if !config.GetDebug() {
log.Out = ioutil.Discard
return log
}
return level
}
func newDevelopmentLogger(config config.AppConfigurer) *logrus.Logger {
log := logrus.New()
log.SetLevel(getLogLevel())
file, err := os.OpenFile(filepath.Join(globalConfigDir(), "development.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
file, err := os.OpenFile("development.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
panic("unable to log to file") // TODO: don't panic (also, remove this call to the `panic` function)
}
@@ -71,34 +38,7 @@ func newDevelopmentLogger(config config.AppConfigurer) *logrus.Logger {
return log
}
func newLogger(config config.AppConfigurer) *logrus.Entry {
var log *logrus.Logger
environment := "production"
if config.GetDebug() || os.Getenv("DEBUG") == "TRUE" {
environment = "development"
log = newDevelopmentLogger(config)
} else {
log = newProductionLogger(config)
}
// highly recommended: tail -f development.log | humanlog
// https://github.com/aybabtme/humanlog
log.Formatter = &logrus.JSONFormatter{}
if config.GetUserConfig().GetString("reporting") == "on" {
// this isn't really a secret token: it only has permission to push new rollbar items
hook := rollrus.NewHook("23432119147a4367abf7c0de2aa99a2d", environment)
log.Hooks.Add(hook)
}
return log.WithFields(logrus.Fields{
"debug": config.GetDebug(),
"version": config.GetVersion(),
"commit": config.GetCommit(),
"buildDate": config.GetBuildDate(),
})
}
// NewApp bootstrap a new application
// NewApp retruns a new applications
func NewApp(config config.AppConfigurer) (*App, error) {
app := &App{
closers: []io.Closer{},
@@ -106,88 +46,27 @@ func NewApp(config config.AppConfigurer) (*App, error) {
}
var err error
app.Log = newLogger(config)
app.Tr = i18n.NewLocalizer(app.Log)
// if we are being called in 'demon' mode, we can just return here
app.ClientContext = os.Getenv("LAZYGIT_CLIENT_COMMAND")
if app.ClientContext != "" {
return app, nil
}
app.OSCommand = commands.NewOSCommand(app.Log, config)
app.Updater, err = updates.NewUpdater(app.Log, config, app.OSCommand, app.Tr)
app.OSCommand, err = commands.NewOSCommand(app.Log)
if err != nil {
return app, err
return nil, err
}
if err := app.setupRepo(); err != nil {
return app, err
app.Tr, err = i18n.NewLocalizer(app.Log)
if err != nil {
return nil, err
}
app.GitCommand, err = commands.NewGitCommand(app.Log, app.OSCommand, app.Tr, app.Config)
app.GitCommand, err = commands.NewGitCommand(app.Log, app.OSCommand)
if err != nil {
return app, err
return nil, err
}
app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config, app.Updater)
app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config)
if err != nil {
return app, err
return nil, err
}
return app, nil
}
func (app *App) setupRepo() error {
// if we are not in a git repo, we ask if we want to `git init`
if err := app.OSCommand.RunCommand("git status"); err != nil {
if !strings.Contains(err.Error(), "Not a git repository") {
return err
}
fmt.Print(app.Tr.SLocalize("CreateRepo"))
response, _ := bufio.NewReader(os.Stdin).ReadString('\n')
if strings.Trim(response, " \n") != "y" {
os.Exit(1)
}
if err := app.OSCommand.RunCommand("git init"); err != nil {
return err
}
}
return nil
}
func (app *App) Run() error {
if app.ClientContext == "INTERACTIVE_REBASE" {
return app.Rebase()
}
if app.ClientContext == "EXIT_IMMEDIATELY" {
os.Exit(0)
}
err := app.Gui.RunWithSubprocesses()
return err
}
// Rebase contains logic for when we've been run in demon mode, meaning we've
// given lazygit as a command for git to call e.g. to edit a file
func (app *App) Rebase() error {
app.Log.Info("Lazygit invoked as interactive rebase demon")
app.Log.Info("args: ", os.Args)
if strings.HasSuffix(os.Args[1], "git-rebase-todo") {
if err := ioutil.WriteFile(os.Args[1], []byte(os.Getenv("LAZYGIT_REBASE_TODO")), 0644); err != nil {
return err
}
} else if strings.HasSuffix(os.Args[1], ".git/COMMIT_EDITMSG") {
// if we are rebasing and squashing, we'll see a COMMIT_EDITMSG
// but in this case we don't need to edit it, so we'll just return
} else {
app.Log.Info("Lazygit demon did not match on any use cases")
}
return nil
}
// Close closes any resources
func (app *App) Close() error {
for _, closer := range app.closers {
@@ -198,22 +77,3 @@ func (app *App) Close() error {
}
return nil
}
// KnownError takes an error and tells us whether it's an error that we know about where we can print a nicely formatted version of it rather than panicking with a stack trace
func (app *App) KnownError(err error) (string, bool) {
errorMessage := err.Error()
mappings := []errorMapping{
{
originalError: "fatal: not a git repository (or any of the parent directories): .git",
newError: app.Tr.SLocalize("notARepository"),
},
}
for _, mapping := range mappings {
if strings.Contains(errorMessage, mapping.originalError) {
return mapping.newError, true
}
}
return "", false
}
+6 -17
View File
@@ -1,11 +1,8 @@
package commands
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -13,21 +10,13 @@ import (
// Branch : A git branch
// duplicating this for now
type Branch struct {
Name string
Recency string
Pushables string
Pullables string
Selected bool
Name string
Recency string
}
// GetDisplayStrings returns the display string of branch
func (b *Branch) GetDisplayStrings(isFocused bool) []string {
displayName := utils.ColoredString(b.Name, b.GetColor())
if isFocused && b.Selected && b.Pushables != "" && b.Pullables != "" {
displayName = fmt.Sprintf("%s ↑%s↓%s", displayName, b.Pushables, b.Pullables)
}
return []string{b.Recency, displayName}
// GetDisplayString returns the dispaly string of branch
func (b *Branch) GetDisplayString() string {
return utils.WithPadding(b.Recency, 4) + utils.ColoredString(b.Name, b.GetColor())
}
// GetColor branch color
@@ -40,7 +29,7 @@ func (b *Branch) GetColor() color.Attribute {
case "hotfix":
return color.FgRed
default:
return theme.DefaultTextColor
return color.FgWhite
}
}
-60
View File
@@ -1,60 +0,0 @@
package commands
import (
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// Commit : A git commit
type Commit struct {
Sha string
Name string
Status string // one of "unpushed", "pushed", "merged", "rebasing" or "selected"
DisplayString string
Action string // one of "", "pick", "edit", "squash", "reword", "drop", "fixup"
Copied bool // to know if this commit is ready to be cherry-picked somewhere
}
// GetDisplayStrings is a function.
func (c *Commit) GetDisplayStrings(isFocused bool) []string {
red := color.New(color.FgRed)
yellow := color.New(color.FgYellow)
green := color.New(color.FgGreen)
blue := color.New(color.FgBlue)
cyan := color.New(color.FgCyan)
defaultColor := color.New(theme.DefaultTextColor)
magenta := color.New(color.FgMagenta)
// for some reason, setting the background to blue pads out the other commits
// horizontally. For the sake of accessibility I'm considering this a feature,
// not a bug
copied := color.New(color.FgCyan, color.BgBlue)
var shaColor *color.Color
switch c.Status {
case "unpushed":
shaColor = red
case "pushed":
shaColor = yellow
case "merged":
shaColor = green
case "rebasing":
shaColor = blue
case "selected":
shaColor = magenta
default:
shaColor = defaultColor
}
if c.Copied {
shaColor = copied
}
actionString := ""
if c.Action != "" {
actionString = cyan.Sprint(utils.WithPadding(c.Action, 7)) + " "
}
return []string{shaColor.Sprint(c.Sha), actionString + defaultColor.Sprint(c.Name)}
}
-13
View File
@@ -1,13 +0,0 @@
package commands
// CommitFile : A git commit file
type CommitFile struct {
Sha string
Name string
DisplayString string
}
// GetDisplayStrings is a function.
func (f *CommitFile) GetDisplayStrings(isFocused bool) []string {
return []string{f.DisplayString}
}
-58
View File
@@ -1,58 +0,0 @@
package commands
import (
"io/ioutil"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
yaml "gopkg.in/yaml.v2"
)
// This file exports dummy constructors for use by tests in other packages
// NewDummyOSCommand creates a new dummy OSCommand for testing
func NewDummyOSCommand() *OSCommand {
return NewOSCommand(NewDummyLog(), NewDummyAppConfig())
}
// NewDummyAppConfig creates a new dummy AppConfig for testing
func NewDummyAppConfig() *config.AppConfig {
appConfig := &config.AppConfig{
Name: "lazygit",
Version: "unversioned",
Commit: "",
BuildDate: "",
Debug: false,
BuildSource: "",
UserConfig: viper.New(),
}
_ = yaml.Unmarshal([]byte{}, appConfig.AppState)
return appConfig
}
// NewDummyLog creates a new dummy Log for testing
func NewDummyLog() *logrus.Entry {
log := logrus.New()
log.Out = ioutil.Discard
return log.WithField("test", "test")
}
// NewDummyGitCommand creates a new dummy GitCommand for testing
func NewDummyGitCommand() *GitCommand {
return NewDummyGitCommandWithOSCommand(NewDummyOSCommand())
}
// NewDummyGitCommandWithOSCommand creates a new dummy GitCommand for testing
func NewDummyGitCommandWithOSCommand(osCommand *OSCommand) *GitCommand {
return &GitCommand{
Log: NewDummyLog(),
OSCommand: osCommand,
Tr: i18n.NewLocalizer(NewDummyLog()),
Config: NewDummyAppConfig(),
getGlobalGitConfig: func(string) (string, error) { return "", nil },
getLocalGitConfig: func(string) (string, error) { return "", nil },
removeFile: func(string) error { return nil },
}
}
-14
View File
@@ -1,14 +0,0 @@
package commands
import "github.com/go-errors/errors"
// WrapError wraps an error for the sake of showing a stack trace at the top level
// the go-errors package, for some reason, does not return nil when you try to wrap
// a non-error, so we're just doing it here
func WrapError(err error) error {
if err == nil {
return err
}
return errors.Wrap(err, 0)
}
-95
View File
@@ -1,95 +0,0 @@
// +build !windows
package commands
import (
"bufio"
"bytes"
"strings"
"unicode/utf8"
"github.com/go-errors/errors"
"github.com/jesseduffield/pty"
)
// RunCommandWithOutputLiveWrapper runs a command and return every word that gets written in stdout
// Output is a function that executes by every word that gets read by bufio
// As return of output you need to give a string that will be written to stdin
// NOTE: If the return data is empty it won't written anything to stdin
func RunCommandWithOutputLiveWrapper(c *OSCommand, command string, output func(string) string) error {
cmd := c.ExecutableFromString(command)
cmd.Env = append(cmd.Env, "LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8")
var stderr bytes.Buffer
cmd.Stderr = &stderr
ptmx, err := pty.Start(cmd)
if err != nil {
return err
}
go func() {
scanner := bufio.NewScanner(ptmx)
scanner.Split(scanWordsWithNewLines)
for scanner.Scan() {
toOutput := strings.Trim(scanner.Text(), " ")
_, _ = ptmx.WriteString(output(toOutput))
}
}()
err = cmd.Wait()
ptmx.Close()
if err != nil {
return errors.New(stderr.String())
}
return nil
}
// scanWordsWithNewLines is a copy of bufio.ScanWords but this also captures new lines
// For specific comments about this function take a look at: bufio.ScanWords
func scanWordsWithNewLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
start := 0
for width := 0; start < len(data); start += width {
var r rune
r, width = utf8.DecodeRune(data[start:])
if !isSpace(r) {
break
}
}
for width, i := 0, start; i < len(data); i += width {
var r rune
r, width = utf8.DecodeRune(data[i:])
if isSpace(r) {
return i + width, data[start:i], nil
}
}
if atEOF && len(data) > start {
return len(data), data[start:], nil
}
return start, nil, nil
}
// isSpace is also copied from the bufio package and has been modified to also captures new lines
// For specific comments about this function take a look at: bufio.isSpace
func isSpace(r rune) bool {
if r <= '\u00FF' {
switch r {
case ' ', '\t', '\v', '\f':
return true
case '\u0085', '\u00A0':
return true
}
return false
}
if '\u2000' <= r && r <= '\u200a' {
return true
}
switch r {
case '\u1680', '\u2028', '\u2029', '\u202f', '\u205f', '\u3000':
return true
}
return false
}
-9
View File
@@ -1,9 +0,0 @@
// +build windows
package commands
// RunCommandWithOutputLiveWrapper runs a command live but because of windows compatibility this command can't be ran there
// TODO: Remove this hack and replace it with a proper way to run commands live on windows
func RunCommandWithOutputLiveWrapper(c *OSCommand, command string, output func(string) string) error {
return c.RunCommand(command)
}
-38
View File
@@ -1,38 +0,0 @@
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
HasInlineMergeConflicts bool
DisplayString string
Type string // one of 'file', 'directory', and 'other'
ShortStatus string // e.g. 'AD', ' A', 'M ', '??'
}
// GetDisplayStrings returns the display string of a file
func (f *File) GetDisplayStrings(isFocused bool) []string {
// potentially inefficient to be instantiating these color
// objects with each render
red := color.New(color.FgRed)
green := color.New(color.FgGreen)
if !f.Tracked && !f.HasStagedChanges {
return []string{red.Sprint(f.DisplayString)}
}
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}
}
+276 -787
View File
File diff suppressed because it is too large Load Diff
+27
View File
@@ -1,5 +1,32 @@
package commands
// File : A staged/unstaged file
// TODO: decide whether to give all of these the Git prefix
type File struct {
Name string
HasStagedChanges bool
HasUnstagedChanges bool
Tracked bool
Deleted bool
HasMergeConflicts bool
DisplayString string
}
// 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
// numbers in the file where the conflict bars appear
type Conflict struct {
File diff suppressed because it is too large Load Diff
+87 -288
View File
@@ -1,116 +1,50 @@
package commands
import (
"io/ioutil"
"errors"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
"sync"
"github.com/go-errors/errors"
"github.com/davecgh/go-spew/spew"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/mgutz/str"
"github.com/sirupsen/logrus"
"github.com/Sirupsen/logrus"
gitconfig "github.com/tcnksm/go-gitconfig"
)
// Platform stores the os state
type Platform struct {
os string
shell string
shellArg string
escapedQuote string
openCommand string
openLinkCommand string
fallbackEscapedQuote string
os string
shell string
shellArg string
escapedQuote string
}
// OSCommand holds all the os commands
type OSCommand struct {
Log *logrus.Entry
Platform *Platform
Config config.AppConfigurer
command func(string, ...string) *exec.Cmd
getGlobalGitConfig func(string) (string, error)
getenv func(string) string
Log *logrus.Logger
Platform *Platform
}
// NewOSCommand os command runner
func NewOSCommand(log *logrus.Entry, config config.AppConfigurer) *OSCommand {
return &OSCommand{
Log: log,
Platform: getPlatform(),
Config: config,
command: exec.Command,
getGlobalGitConfig: gitconfig.Global,
getenv: os.Getenv,
func NewOSCommand(log *logrus.Logger) (*OSCommand, error) {
osCommand := &OSCommand{
Log: log,
Platform: getPlatform(),
}
}
// SetCommand sets the command function used by the struct.
// To be used for testing only
func (c *OSCommand) SetCommand(cmd func(string, ...string) *exec.Cmd) {
c.command = cmd
return osCommand, nil
}
// RunCommandWithOutput wrapper around commands returning their output and error
func (c *OSCommand) RunCommandWithOutput(command string) (string, error) {
c.Log.WithField("command", command).Info("RunCommand")
cmd := c.ExecutableFromString(command)
return sanitisedCommandOutput(cmd.CombinedOutput())
}
// RunExecutableWithOutput runs an executable file and returns its output
func (c *OSCommand) RunExecutableWithOutput(cmd *exec.Cmd) (string, error) {
return sanitisedCommandOutput(cmd.CombinedOutput())
}
// RunExecutable runs an executable file and returns an error if there was one
func (c *OSCommand) RunExecutable(cmd *exec.Cmd) error {
_, err := c.RunExecutableWithOutput(cmd)
return err
}
// ExecutableFromString takes a string like `git status` and returns an executable command for it
func (c *OSCommand) ExecutableFromString(commandStr string) *exec.Cmd {
splitCmd := str.ToArgv(commandStr)
cmd := c.command(splitCmd[0], splitCmd[1:]...)
cmd.Env = append(os.Environ(), "GIT_OPTIONAL_LOCKS=0")
return cmd
}
// RunCommandWithOutputLive runs RunCommandWithOutputLiveWrapper
func (c *OSCommand) RunCommandWithOutputLive(command string, output func(string) string) error {
return RunCommandWithOutputLiveWrapper(c, command, output)
}
// DetectUnamePass detect a username / password question in a command
// ask is a function that gets executen when this function detect you need to fillin a password
// The ask argument will be "username" or "password" and expects the user's password or username back
func (c *OSCommand) DetectUnamePass(command string, ask func(string) string) error {
ttyText := ""
errMessage := c.RunCommandWithOutputLive(command, func(word string) string {
ttyText = ttyText + " " + word
prompts := map[string]string{
"password": `Password\s*for\s*'.+':`,
"username": `Username\s*for\s*'.+':`,
}
for askFor, pattern := range prompts {
if match, _ := regexp.MatchString(pattern, ttyText); match {
ttyText = ""
return ask(askFor)
}
}
return ""
})
return errMessage
splitCmd := str.ToArgv(command)
c.Log.Info(splitCmd)
cmdOut, err := exec.Command(splitCmd[0], splitCmd[1:]...).CombinedOutput()
return sanitisedCommandOutput(cmdOut, err)
}
// RunCommand runs a command and just returns the error
@@ -119,26 +53,16 @@ func (c *OSCommand) RunCommand(command string) error {
return err
}
// FileType tells us if the file is a file, directory or other
func (c *OSCommand) FileType(path string) string {
fileInfo, err := os.Stat(path)
if err != nil {
return "other"
}
if fileInfo.IsDir() {
return "directory"
}
return "file"
}
// RunDirectCommand wrapper around direct commands
func (c *OSCommand) RunDirectCommand(command string) (string, error) {
c.Log.WithField("command", command).Info("RunDirectCommand")
args := str.ToArgv(c.Platform.shellArg + " " + command)
c.Log.Info(spew.Sdump(args))
return sanitisedCommandOutput(
c.command(c.Platform.shell, c.Platform.shellArg, command).
CombinedOutput(),
)
cmdOut, err := exec.
Command(c.Platform.shell, args...).
CombinedOutput()
return sanitisedCommandOutput(cmdOut, err)
}
func sanitisedCommandOutput(output []byte, err error) (string, error) {
@@ -146,48 +70,80 @@ func sanitisedCommandOutput(output []byte, err error) (string, error) {
if err != nil {
// errors like 'exit status 1' are not very useful so we'll create an error
// from the combined output
if outputString == "" {
return "", WrapError(err)
}
return outputString, errors.New(outputString)
}
return outputString, nil
}
// OpenFile opens a file with the given
func (c *OSCommand) OpenFile(filename string) error {
commandTemplate := c.Config.GetUserConfig().GetString("os.openCommand")
templateValues := map[string]string{
"filename": c.Quote(filename),
func getPlatform() *Platform {
switch runtime.GOOS {
case "windows":
return &Platform{
os: "windows",
shell: "cmd",
shellArg: "/c",
escapedQuote: "\\\"",
}
default:
return &Platform{
os: runtime.GOOS,
shell: "bash",
shellArg: "-c",
escapedQuote: "\"",
}
}
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
err := c.RunCommand(command)
return err
}
// OpenLink opens a file with the given
func (c *OSCommand) OpenLink(link string) error {
commandTemplate := c.Config.GetUserConfig().GetString("os.openLinkCommand")
templateValues := map[string]string{
"link": c.Quote(link),
// GetOpenCommand get open command
func (c *OSCommand) GetOpenCommand() (string, string, error) {
//NextStep open equivalents: xdg-open (linux), cygstart (cygwin), open (OSX)
trailMap := map[string]string{
"xdg-open": " &>/dev/null &",
"cygstart": "",
"open": "",
}
for name, trail := range trailMap {
if err := c.RunCommand("which " + name); err == nil {
return name, trail, nil
}
}
return "", "", errors.New("Unsure what command to use to open this file")
}
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
err := c.RunCommand(command)
return err
// VsCodeOpenFile opens the file in code, with the -r flag to open in the
// current window
// each of these open files needs to have the same function signature because
// they're being passed as arguments into another function,
// but only editFile actually returns a *exec.Cmd
func (c *OSCommand) VsCodeOpenFile(filename string) (*exec.Cmd, error) {
return nil, c.RunCommand("code -r " + filename)
}
// SublimeOpenFile opens the filein sublime
// may be deprecated in the future
func (c *OSCommand) SublimeOpenFile(filename string) (*exec.Cmd, error) {
return nil, c.RunCommand("subl " + filename)
}
// OpenFile opens a file with the given
func (c *OSCommand) OpenFile(filename string) (*exec.Cmd, error) {
cmdName, cmdTrail, err := c.GetOpenCommand()
if err != nil {
return nil, err
}
err = c.RunCommand(cmdName + " " + c.Quote(filename) + cmdTrail) // TODO: test on linux
return nil, err
}
// EditFile opens a file in a subprocess using whatever editor is available,
// falling back to core.editor, VISUAL, EDITOR, then vi
func (c *OSCommand) EditFile(filename string) (*exec.Cmd, error) {
editor, _ := c.getGlobalGitConfig("core.editor")
editor, _ := gitconfig.Global("core.editor")
if editor == "" {
editor = c.getenv("VISUAL")
editor = os.Getenv("VISUAL")
}
if editor == "" {
editor = c.getenv("EDITOR")
editor = os.Getenv("EDITOR")
}
if editor == "" {
if err := c.RunCommand("which vi"); err == nil {
@@ -197,174 +153,17 @@ func (c *OSCommand) EditFile(filename string) (*exec.Cmd, error) {
if editor == "" {
return nil, errors.New("No editor defined in $VISUAL, $EDITOR, or git config")
}
return c.PrepareSubProcess(editor, filename), nil
return c.PrepareSubProcess(editor, filename)
}
// PrepareSubProcess iniPrepareSubProcessrocess then tells the Gui to switch to it
// TODO: see if this needs to exist, given that ExecutableFromString does the same things
func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *exec.Cmd {
cmd := c.command(cmdName, commandArgs...)
if cmd != nil {
cmd.Env = append(os.Environ(), "GIT_OPTIONAL_LOCKS=0")
}
return cmd
func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) (*exec.Cmd, error) {
subprocess := exec.Command(cmdName, commandArgs...)
return subprocess, nil
}
// Quote wraps a message in platform-specific quotation marks
func (c *OSCommand) Quote(message string) string {
message = strings.Replace(message, "`", "\\`", -1)
escapedQuote := c.Platform.escapedQuote
if strings.Contains(message, c.Platform.escapedQuote) {
escapedQuote = c.Platform.fallbackEscapedQuote
}
return escapedQuote + message + escapedQuote
}
// Unquote removes wrapping quotations marks if they are present
// this is needed for removing quotes from staged filenames with spaces
func (c *OSCommand) Unquote(message string) string {
return strings.Replace(message, `"`, "", -1)
}
// AppendLineToFile adds a new line in file
func (c *OSCommand) AppendLineToFile(filename, line string) error {
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
return WrapError(err)
}
defer f.Close()
_, err = f.WriteString("\n" + line)
if err != nil {
return WrapError(err)
}
return nil
}
// CreateTempFile writes a string to a new temp file and returns the file's name
func (c *OSCommand) CreateTempFile(filename, content string) (string, error) {
tmpfile, err := ioutil.TempFile("", filename)
if err != nil {
c.Log.Error(err)
return "", WrapError(err)
}
if _, err := tmpfile.WriteString(content); err != nil {
c.Log.Error(err)
return "", WrapError(err)
}
if err := tmpfile.Close(); err != nil {
c.Log.Error(err)
return "", WrapError(err)
}
return tmpfile.Name(), nil
}
// Remove removes a file or directory at the specified path
func (c *OSCommand) Remove(filename string) error {
err := os.RemoveAll(filename)
return WrapError(err)
}
// FileExists checks whether a file exists at the specified path
func (c *OSCommand) FileExists(path string) (bool, error) {
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
return true, nil
}
// RunPreparedCommand takes a pointer to an exec.Cmd and runs it
// this is useful if you need to give your command some environment variables
// before running it
func (c *OSCommand) RunPreparedCommand(cmd *exec.Cmd) error {
out, err := cmd.CombinedOutput()
outString := string(out)
c.Log.Info(outString)
if err != nil {
if len(outString) == 0 {
return err
}
return errors.New(outString)
}
return nil
}
// GetLazygitPath returns the path of the currently executed file
func (c *OSCommand) GetLazygitPath() string {
ex, err := os.Executable() // get the executable path for git to use
if err != nil {
ex = os.Args[0] // fallback to the first call argument if needed
}
return filepath.ToSlash(ex)
}
// RunCustomCommand returns the pointer to a custom command
func (c *OSCommand) RunCustomCommand(command string) *exec.Cmd {
return c.PrepareSubProcess(c.Platform.shell, c.Platform.shellArg, command)
}
// PipeCommands runs a heap of commands and pipes their inputs/outputs together like A | B | C
func (c *OSCommand) PipeCommands(commandStrings ...string) error {
cmds := make([]*exec.Cmd, len(commandStrings))
for i, str := range commandStrings {
cmds[i] = c.ExecutableFromString(str)
}
for i := 0; i < len(cmds)-1; i++ {
stdout, err := cmds[i].StdoutPipe()
if err != nil {
return err
}
cmds[i+1].Stdin = stdout
}
// keeping this here in case I adapt this code for some other purpose in the future
// cmds[len(cmds)-1].Stdout = os.Stdout
finalErrors := []string{}
wg := sync.WaitGroup{}
wg.Add(len(cmds))
for _, cmd := range cmds {
currentCmd := cmd
go func() {
stderr, err := currentCmd.StderrPipe()
if err != nil {
c.Log.Error(err)
}
if err := currentCmd.Start(); err != nil {
c.Log.Error(err)
}
if b, err := ioutil.ReadAll(stderr); err == nil {
if len(b) > 0 {
finalErrors = append(finalErrors, string(b))
}
}
if err := currentCmd.Wait(); err != nil {
c.Log.Error(err)
}
wg.Done()
}()
}
wg.Wait()
if len(finalErrors) > 0 {
return errors.New(strings.Join(finalErrors, "\n"))
}
return nil
return c.Platform.escapedQuote + message + c.Platform.escapedQuote
}
-19
View File
@@ -1,19 +0,0 @@
// +build !windows
package commands
import (
"runtime"
)
func getPlatform() *Platform {
return &Platform{
os: runtime.GOOS,
shell: "bash",
shellArg: "-c",
escapedQuote: "'",
openCommand: "open {{filename}}",
openLinkCommand: "open {{link}}",
fallbackEscapedQuote: "\"",
}
}
+9 -370
View File
@@ -1,377 +1,16 @@
package commands
import (
"io/ioutil"
"os"
"os/exec"
"testing"
import "testing"
"github.com/stretchr/testify/assert"
)
// TestOSCommandRunCommandWithOutput is a function.
func TestOSCommandRunCommandWithOutput(t *testing.T) {
type scenario struct {
command string
test func(string, error)
func TestQuote(t *testing.T) {
osCommand := &OSCommand{
Log: nil,
Platform: getPlatform(),
}
scenarios := []scenario{
{
"echo -n '123'",
func(output string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "123", output)
},
},
{
"rmdir unexisting-folder",
func(output string, err error) {
assert.Regexp(t, "rmdir.*unexisting-folder.*", err.Error())
},
},
}
for _, s := range scenarios {
s.test(NewDummyOSCommand().RunCommandWithOutput(s.command))
}
}
// TestOSCommandRunCommand is a function.
func TestOSCommandRunCommand(t *testing.T) {
type scenario struct {
command string
test func(error)
}
scenarios := []scenario{
{
"rmdir unexisting-folder",
func(err error) {
assert.Regexp(t, "rmdir.*unexisting-folder.*", err.Error())
},
},
}
for _, s := range scenarios {
s.test(NewDummyOSCommand().RunCommand(s.command))
}
}
// TestOSCommandOpenFile is a function.
func TestOSCommandOpenFile(t *testing.T) {
type scenario struct {
filename string
command func(string, ...string) *exec.Cmd
test func(error)
}
scenarios := []scenario{
{
"test",
func(name string, arg ...string) *exec.Cmd {
return exec.Command("exit", "1")
},
func(err error) {
assert.Error(t, err)
},
},
{
"test",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "open", name)
assert.Equal(t, []string{"test"}, arg)
return exec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
},
{
"filename with spaces",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "open", name)
assert.Equal(t, []string{"filename with spaces"}, arg)
return exec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
OSCmd := NewDummyOSCommand()
OSCmd.command = s.command
OSCmd.Config.GetUserConfig().Set("os.openCommand", "open {{filename}}")
s.test(OSCmd.OpenFile(s.filename))
}
}
// TestOSCommandEditFile is a function.
func TestOSCommandEditFile(t *testing.T) {
type scenario struct {
filename string
command func(string, ...string) *exec.Cmd
getenv func(string) string
getGlobalGitConfig func(string) (string, error)
test func(*exec.Cmd, error)
}
scenarios := []scenario{
{
"test",
func(name string, arg ...string) *exec.Cmd {
return exec.Command("exit", "1")
},
func(env string) string {
return ""
},
func(cf string) (string, error) {
return "", nil
},
func(cmd *exec.Cmd, err error) {
assert.EqualError(t, err, "No editor defined in $VISUAL, $EDITOR, or git config")
},
},
{
"test",
func(name string, arg ...string) *exec.Cmd {
if name == "which" {
return exec.Command("exit", "1")
}
assert.EqualValues(t, "nano", name)
return nil
},
func(env string) string {
return ""
},
func(cf string) (string, error) {
return "nano", nil
},
func(cmd *exec.Cmd, err error) {
assert.NoError(t, err)
},
},
{
"test",
func(name string, arg ...string) *exec.Cmd {
if name == "which" {
return exec.Command("exit", "1")
}
assert.EqualValues(t, "nano", name)
return nil
},
func(env string) string {
if env == "VISUAL" {
return "nano"
}
return ""
},
func(cf string) (string, error) {
return "", nil
},
func(cmd *exec.Cmd, err error) {
assert.NoError(t, err)
},
},
{
"test",
func(name string, arg ...string) *exec.Cmd {
if name == "which" {
return exec.Command("exit", "1")
}
assert.EqualValues(t, "emacs", name)
return nil
},
func(env string) string {
if env == "EDITOR" {
return "emacs"
}
return ""
},
func(cf string) (string, error) {
return "", nil
},
func(cmd *exec.Cmd, err error) {
assert.NoError(t, err)
},
},
{
"test",
func(name string, arg ...string) *exec.Cmd {
if name == "which" {
return exec.Command("echo")
}
assert.EqualValues(t, "vi", name)
return nil
},
func(env string) string {
return ""
},
func(cf string) (string, error) {
return "", nil
},
func(cmd *exec.Cmd, err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
OSCmd := NewDummyOSCommand()
OSCmd.command = s.command
OSCmd.getGlobalGitConfig = s.getGlobalGitConfig
OSCmd.getenv = s.getenv
s.test(OSCmd.EditFile(s.filename))
}
}
// TestOSCommandQuote is a function.
func TestOSCommandQuote(t *testing.T) {
osCommand := NewDummyOSCommand()
actual := osCommand.Quote("hello `test`")
test := "hello `test`"
expected := osCommand.Platform.escapedQuote + "hello \\`test\\`" + osCommand.Platform.escapedQuote
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)
}
// TestOSCommandQuoteDoubleQuote 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)
}
// TestOSCommandUnquote is a function.
func TestOSCommandUnquote(t *testing.T) {
osCommand := NewDummyOSCommand()
actual := osCommand.Unquote(`hello "test"`)
expected := "hello test"
assert.EqualValues(t, expected, actual)
}
// TestOSCommandFileType is a function.
func TestOSCommandFileType(t *testing.T) {
type scenario struct {
path string
setup func()
test func(string)
}
scenarios := []scenario{
{
"testFile",
func() {
if _, err := os.Create("testFile"); err != nil {
panic(err)
}
},
func(output string) {
assert.EqualValues(t, "file", output)
},
},
{
"file with spaces",
func() {
if _, err := os.Create("file with spaces"); err != nil {
panic(err)
}
},
func(output string) {
assert.EqualValues(t, "file", output)
},
},
{
"testDirectory",
func() {
if err := os.Mkdir("testDirectory", 0644); err != nil {
panic(err)
}
},
func(output string) {
assert.EqualValues(t, "directory", output)
},
},
{
"nonExistant",
func() {},
func(output string) {
assert.EqualValues(t, "other", output)
},
},
}
for _, s := range scenarios {
s.setup()
s.test(NewDummyOSCommand().FileType(s.path))
_ = os.RemoveAll(s.path)
}
}
func TestOSCommandCreateTempFile(t *testing.T) {
type scenario struct {
testName string
filename string
content string
test func(string, error)
}
scenarios := []scenario{
{
"valid case",
"filename",
"content",
func(path string, err error) {
assert.NoError(t, err)
content, err := ioutil.ReadFile(path)
assert.NoError(t, err)
assert.Equal(t, "content", string(content))
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
s.test(NewDummyOSCommand().CreateTempFile(s.filename, s.content))
})
test = osCommand.Quote(test)
if test != expected {
t.Error("Expected " + expected + ", got " + test)
}
}
-11
View File
@@ -1,11 +0,0 @@
package commands
func getPlatform() *Platform {
return &Platform{
os: "windows",
shell: "cmd",
shellArg: "/c",
escapedQuote: `\"`,
fallbackEscapedQuote: "\\'",
}
}
-106
View File
@@ -1,106 +0,0 @@
package commands
import (
"fmt"
"strings"
"github.com/go-errors/errors"
)
// Service is a service that repository is on (Github, Bitbucket, ...)
type Service struct {
Name string
PullRequestURL string
}
// PullRequest opens a link in browser to create new pull request
// with selected branch
type PullRequest struct {
GitServices []*Service
GitCommand *GitCommand
}
// RepoInformation holds some basic information about the repo
type RepoInformation struct {
Owner string
Repository string
}
func getServices() []*Service {
return []*Service{
{
Name: "github.com",
PullRequestURL: "https://github.com/%s/%s/compare/%s?expand=1",
},
{
Name: "bitbucket.org",
PullRequestURL: "https://bitbucket.org/%s/%s/pull-requests/new?source=%s&t=1",
},
{
Name: "gitlab.com",
PullRequestURL: "https://gitlab.com/%s/%s/merge_requests/new?merge_request[source_branch]=%s",
},
}
}
// NewPullRequest creates new instance of PullRequest
func NewPullRequest(gitCommand *GitCommand) *PullRequest {
return &PullRequest{
GitServices: getServices(),
GitCommand: gitCommand,
}
}
// Create opens link to new pull request in browser
func (pr *PullRequest) Create(branch *Branch) error {
branchExistsOnRemote := pr.GitCommand.CheckRemoteBranchExists(branch)
if !branchExistsOnRemote {
return errors.New(pr.GitCommand.Tr.SLocalize("NoBranchOnRemote"))
}
repoURL := pr.GitCommand.GetRemoteURL()
var gitService *Service
for _, service := range pr.GitServices {
if strings.Contains(repoURL, service.Name) {
gitService = service
break
}
}
if gitService == nil {
return errors.New(pr.GitCommand.Tr.SLocalize("UnsupportedGitService"))
}
repoInfo := getRepoInfoFromURL(repoURL)
return pr.GitCommand.OSCommand.OpenLink(fmt.Sprintf(
gitService.PullRequestURL, repoInfo.Owner, repoInfo.Repository, branch.Name,
))
}
func getRepoInfoFromURL(url string) *RepoInformation {
isHTTP := strings.HasPrefix(url, "http")
if isHTTP {
splits := strings.Split(url, "/")
owner := splits[len(splits)-2]
repo := strings.TrimSuffix(splits[len(splits)-1], ".git")
return &RepoInformation{
Owner: owner,
Repository: repo,
}
}
tmpSplit := strings.Split(url, ":")
splits := strings.Split(tmpSplit[1], "/")
owner := splits[0]
repo := strings.TrimSuffix(splits[1], ".git")
return &RepoInformation{
Owner: owner,
Repository: repo,
}
}
-154
View File
@@ -1,154 +0,0 @@
package commands
import (
"os/exec"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
// TestGetRepoInfoFromURL is a function.
func TestGetRepoInfoFromURL(t *testing.T) {
type scenario struct {
testName string
repoURL string
test func(*RepoInformation)
}
scenarios := []scenario{
{
"Returns repository information for git remote url",
"git@github.com:petersmith/super_calculator",
func(repoInfo *RepoInformation) {
assert.EqualValues(t, repoInfo.Owner, "petersmith")
assert.EqualValues(t, repoInfo.Repository, "super_calculator")
},
},
{
"Returns repository information for http remote url",
"https://my_username@bitbucket.org/johndoe/social_network.git",
func(repoInfo *RepoInformation) {
assert.EqualValues(t, repoInfo.Owner, "johndoe")
assert.EqualValues(t, repoInfo.Repository, "social_network")
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
s.test(getRepoInfoFromURL(s.repoURL))
})
}
}
// TestCreatePullRequest is a function.
func TestCreatePullRequest(t *testing.T) {
type scenario struct {
testName string
branch *Branch
command func(string, ...string) *exec.Cmd
test func(err error)
}
scenarios := []scenario{
{
"Opens a link to new pull request on bitbucket",
&Branch{
Name: "feature/profile-page",
},
func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return exec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
}
assert.Equal(t, cmd, "open")
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1"})
return exec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
},
{
"Opens a link to new pull request on bitbucket with http remote url",
&Branch{
Name: "feature/events",
},
func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return exec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
}
assert.Equal(t, cmd, "open")
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1"})
return exec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
},
{
"Opens a link to new pull request on github",
&Branch{
Name: "feature/sum-operation",
},
func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return exec.Command("echo", "git@github.com:peter/calculator.git")
}
assert.Equal(t, cmd, "open")
assert.Equal(t, args, []string{"https://github.com/peter/calculator/compare/feature/sum-operation?expand=1"})
return exec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
},
{
"Opens a link to new pull request on gitlab",
&Branch{
Name: "feature/ui",
},
func(cmd string, args ...string) *exec.Cmd {
// Handle git remote url call
if strings.HasPrefix(cmd, "git") {
return exec.Command("echo", "git@gitlab.com:peter/calculator.git")
}
assert.Equal(t, cmd, "open")
assert.Equal(t, args, []string{"https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"})
return exec.Command("echo")
},
func(err error) {
assert.NoError(t, err)
},
},
{
"Throws an error if git service is unsupported",
&Branch{
Name: "feature/divide-operation",
},
func(cmd string, args ...string) *exec.Cmd {
return exec.Command("echo", "git@something.com:peter/calculator.git")
},
func(err error) {
assert.Error(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCommand := NewDummyGitCommand()
gitCommand.OSCommand.command = s.command
gitCommand.OSCommand.Config.GetUserConfig().Set("os.openLinkCommand", "open {{link}}")
dummyPullRequest := NewPullRequest(gitCommand)
s.test(dummyPullRequest.Create(s.branch))
})
}
}
-13
View File
@@ -1,13 +0,0 @@
package commands
// StashEntry : A git stash entry
type StashEntry struct {
Index int
Name string
DisplayString string
}
// GetDisplayStrings returns the display string of branch
func (s *StashEntry) GetDisplayStrings(isFocused bool) []string {
return []string{s.DisplayString}
}
View File
View File
+72 -187
View File
@@ -2,27 +2,19 @@ package config
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"github.com/shibukawa/configdir"
"github.com/spf13/viper"
yaml "gopkg.in/yaml.v2"
)
// AppConfig contains the base configuration fields required for lazygit.
type AppConfig struct {
Debug bool `long:"debug" env:"DEBUG" default:"false"`
Version string `long:"version" env:"VERSION" default:"unversioned"`
Commit string `long:"commit" env:"COMMIT"`
BuildDate string `long:"build-date" env:"BUILD_DATE"`
Name string `long:"name" env:"NAME" default:"lazygit"`
BuildSource string `long:"build-source" env:"BUILD_SOURCE" default:""`
UserConfig *viper.Viper
UserConfigDir string
AppState *AppState
IsNewRepo bool
Debug bool `long:"debug" env:"DEBUG" default:"false"`
Version string `long:"version" env:"VERSION" default:"unversioned"`
Commit string `long:"commit" env:"COMMIT"`
BuildDate string `long:"build-date" env:"BUILD_DATE"`
Name string `long:"name" env:"NAME" default:"lazygit"`
UserConfig *viper.Viper
}
// AppConfigurer interface allows individual app config structs to inherit Fields
@@ -33,58 +25,28 @@ type AppConfigurer interface {
GetCommit() string
GetBuildDate() string
GetName() string
GetBuildSource() string
GetUserConfig() *viper.Viper
GetUserConfigDir() string
GetAppState() *AppState
WriteToUserConfig(string, string) error
SaveAppState() error
LoadAppState() error
SetIsNewRepo(bool)
GetIsNewRepo() bool
InsertToUserConfig(string, string) error
}
// NewAppConfig makes a new app config
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag bool) (*AppConfig, error) {
userConfig, userConfigPath, err := LoadConfig("config", true)
func NewAppConfig(name, version, commit, date string, debuggingFlag *bool) (*AppConfig, error) {
userConfig, err := LoadUserConfig()
if err != nil {
return nil, err
}
if os.Getenv("DEBUG") == "TRUE" {
debuggingFlag = true
panic(err)
}
appConfig := &AppConfig{
Name: "lazygit",
Version: version,
Commit: commit,
BuildDate: date,
Debug: debuggingFlag,
BuildSource: buildSource,
UserConfig: userConfig,
UserConfigDir: filepath.Dir(userConfigPath),
AppState: &AppState{},
IsNewRepo: false,
Name: "lazygit",
Version: version,
Commit: commit,
BuildDate: date,
Debug: *debuggingFlag,
UserConfig: userConfig,
}
if err := appConfig.LoadAppState(); err != nil {
return nil, err
}
return appConfig, nil
}
// GetIsNewRepo returns known repo boolean
func (c *AppConfig) GetIsNewRepo() bool {
return c.IsNewRepo
}
// SetIsNewRepo set if the current repo is known
func (c *AppConfig) SetIsNewRepo(toSet bool) {
c.IsNewRepo = toSet
}
// GetDebug returns debug flag
func (c *AppConfig) GetDebug() bool {
return c.Debug
@@ -110,175 +72,98 @@ func (c *AppConfig) GetName() string {
return c.Name
}
// GetBuildSource returns the source of the build. For builds from goreleaser
// this will be binaryBuild
func (c *AppConfig) GetBuildSource() string {
return c.BuildSource
}
// GetUserConfig returns the user config
func (c *AppConfig) GetUserConfig() *viper.Viper {
return c.UserConfig
}
// GetAppState returns the app state
func (c *AppConfig) GetAppState() *AppState {
return c.AppState
}
func (c *AppConfig) GetUserConfigDir() string {
return c.UserConfigDir
}
func newViper(filename string) (*viper.Viper, error) {
func newViper() (*viper.Viper, error) {
v := viper.New()
v.SetConfigType("yaml")
v.SetConfigName(filename)
v.SetConfigName("config")
return v, nil
}
// LoadConfig gets the user's config
func LoadConfig(filename string, withDefaults bool) (*viper.Viper, string, error) {
v, err := newViper(filename)
// LoadUserConfig gets the user's config
func LoadUserConfig() (*viper.Viper, error) {
v, err := newViper()
if err != nil {
return nil, "", err
panic(err)
}
if withDefaults {
if err = LoadDefaults(v, GetDefaultConfig()); err != nil {
return nil, "", err
}
if err = LoadDefaults(v, GetPlatformDefaultConfig()); err != nil {
return nil, "", err
}
if err = LoadDefaultConfig(v); err != nil {
return nil, err
}
configPath, err := LoadAndMergeFile(v, filename+".yml")
if err != nil {
return nil, "", err
if err = LoadUserConfigFromFile(v); err != nil {
return nil, err
}
return v, configPath, nil
return v, nil
}
// LoadDefaults loads in the defaults defined in this file
func LoadDefaults(v *viper.Viper, defaults []byte) error {
return v.MergeConfig(bytes.NewBuffer(defaults))
// LoadDefaultConfig loads in the defaults defined in this file
func LoadDefaultConfig(v *viper.Viper) error {
defaults := getDefaultConfig()
return v.ReadConfig(bytes.NewBuffer(defaults))
}
func prepareConfigFile(filename string) (string, error) {
// LoadUserConfigFromFile Loads the user config from their config file, creating
// the file as an empty config if it does not exist
func LoadUserConfigFromFile(v *viper.Viper) error {
// chucking my name there is not for vanity purposes, the xdg spec (and that
// function) requires a vendor name. May as well line up with github
configDirs := configdir.New("jesseduffield", "lazygit")
folder := configDirs.QueryFolderContainsFile(filename)
folder := configDirs.QueryFolderContainsFile("config.yml")
if folder == nil {
// create the file as empty
// create the file as an empty config and load it
folders := configDirs.QueryFolders(configdir.Global)
if err := folders[0].WriteFile(filename, []byte{}); err != nil {
return "", err
if err := folders[0].WriteFile("config.yml", []byte{}); err != nil {
return err
}
folder = configDirs.QueryFolderContainsFile(filename)
folder = configDirs.QueryFolderContainsFile("config.yml")
}
return filepath.Join(folder.Path, filename), nil
v.AddConfigPath(folder.Path)
if err := v.MergeInConfig(); err != nil {
return err
}
return nil
}
// LoadAndMergeFile Loads the config/state file, creating
// the file has an empty one if it does not exist
func LoadAndMergeFile(v *viper.Viper, filename string) (string, error) {
configPath, err := prepareConfigFile(filename)
if err != nil {
return "", err
}
v.AddConfigPath(filepath.Dir(configPath))
return configPath, v.MergeInConfig()
}
// WriteToUserConfig adds a key/value pair to the user's config and saves it
func (c *AppConfig) WriteToUserConfig(key, value string) error {
// reloading the user config directly (without defaults) so that we're not
// writing any defaults back to the user's config
v, _, err := LoadConfig("config", false)
// InsertToUserConfig adds a key/value pair to the user's config and saves it
func (c *AppConfig) InsertToUserConfig(key, value string) error {
// making a new viper object so that we're not writing any defaults back
// to the user's config file
v, err := newViper()
if err != nil {
return err
}
if err = LoadUserConfigFromFile(v); err != nil {
return err
}
v.Set(key, value)
return v.WriteConfig()
}
// SaveAppState marshalls the AppState struct and writes it to the disk
func (c *AppConfig) SaveAppState() error {
marshalledAppState, err := yaml.Marshal(c.AppState)
if err != nil {
if err := v.WriteConfig(); err != nil {
return err
}
filepath, err := prepareConfigFile("state.yml")
if err != nil {
return err
}
return ioutil.WriteFile(filepath, marshalledAppState, 0644)
return nil
}
// LoadAppState loads recorded AppState from file
func (c *AppConfig) LoadAppState() error {
filepath, err := prepareConfigFile("state.yml")
if err != nil {
return err
}
appStateBytes, err := ioutil.ReadFile(filepath)
if err != nil {
return err
}
if len(appStateBytes) == 0 {
return yaml.Unmarshal(getDefaultAppState(), c.AppState)
}
return yaml.Unmarshal(appStateBytes, c.AppState)
}
// GetDefaultConfig returns the application default configuration
func GetDefaultConfig() []byte {
return []byte(
`gui:
## stuff relating to the UI
scrollHeight: 2
scrollPastBottom: true
mouseEvents: false # will default to true when the feature is complete
theme:
lightTheme: false
activeBorderColor:
- white
- bold
inactiveBorderColor:
- white
optionsTextColor:
- blue
commitLength:
show: true
git:
merging:
manualCommit: false
skipHookPrefix: 'WIP'
autoFetch: true
update:
method: prompt # can be: prompt | background | never
days: 14 # how often a update is checked for
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
confirmOnQuit: false
`)
}
// AppState stores data between runs of the app like when the last update check
// was performed and which other repos have been checked out
type AppState struct {
LastUpdateCheck int64
RecentRepos []string
}
func getDefaultAppState() []byte {
func getDefaultConfig() []byte {
return []byte(`
lastUpdateCheck: 0
recentRepos: []
`)
gui:
## stuff relating to the UI
scrollHeight: 2
theme:
activeBorderColor:
- white
- bold
inactiveBorderColor:
- white
optionsTextColor:
- blue
git:
# stuff relating to git
os:
# stuff relating to the OS
`)
}
// // commenting this out until we use it again
-11
View File
@@ -1,11 +0,0 @@
// +build !windows,!linux
package config
// GetPlatformDefaultConfig gets the defaults for the platform
func GetPlatformDefaultConfig() []byte {
return []byte(
`os:
openCommand: 'open {{filename}}'
openLinkCommand: 'open {{link}}'`)
}
-9
View File
@@ -1,9 +0,0 @@
package config
// GetPlatformDefaultConfig gets the defaults for the platform
func GetPlatformDefaultConfig() []byte {
return []byte(
`os:
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'
openLinkCommand: 'sh -c "xdg-open {{link}} >/dev/null"'`)
}
-9
View File
@@ -1,9 +0,0 @@
package config
// GetPlatformDefaultConfig gets the defaults for the platform
func GetPlatformDefaultConfig() []byte {
return []byte(
`os:
openCommand: 'cmd /c "start "" {{filename}}"'
openLinkCommand: 'cmd /c "start "" {{link}}"'`)
}
+31 -33
View File
@@ -7,7 +7,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
"github.com/Sirupsen/logrus"
"gopkg.in/src-d/go-git.v4/plumbing"
)
@@ -20,34 +20,35 @@ import (
// our safe branches, then add the remaining safe branches, ensuring uniqueness
// along the way
// if we find out we need to use one of these functions in the git.go file, we
// can just pull them out of here and put them there and then call them from in here
// BranchListBuilder returns a list of Branch objects for the current repo
type BranchListBuilder struct {
Log *logrus.Entry
Log *logrus.Logger
GitCommand *commands.GitCommand
}
// NewBranchListBuilder builds a new branch list builder
func NewBranchListBuilder(log *logrus.Entry, gitCommand *commands.GitCommand) (*BranchListBuilder, error) {
func NewBranchListBuilder(log *logrus.Logger, gitCommand *commands.GitCommand) (*BranchListBuilder, error) {
return &BranchListBuilder{
Log: log,
GitCommand: gitCommand,
}, nil
}
func (b *BranchListBuilder) obtainCurrentBranch() *commands.Branch {
branchName, err := b.GitCommand.CurrentBranchName()
func (b *BranchListBuilder) obtainCurrentBranch() commands.Branch {
// I used go-git for this, but that breaks if you've just done a git init,
// even though you're on 'master'
branchName, err := b.GitCommand.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD")
if err != nil {
panic(err.Error())
branchName, err = b.GitCommand.OSCommand.RunCommandWithOutput("git rev-parse --short HEAD")
if err != nil {
panic(err.Error())
}
}
return &commands.Branch{Name: strings.TrimSpace(branchName)}
return commands.Branch{Name: strings.TrimSpace(branchName), Recency: " *"}
}
func (b *BranchListBuilder) obtainReflogBranches() []*commands.Branch {
branches := make([]*commands.Branch, 0)
func (b *BranchListBuilder) obtainReflogBranches() []commands.Branch {
branches := make([]commands.Branch, 0)
rawString, err := b.GitCommand.OSCommand.RunCommandWithOutput("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD")
if err != nil {
return branches
@@ -57,29 +58,29 @@ func (b *BranchListBuilder) obtainReflogBranches() []*commands.Branch {
for _, line := range branchLines {
timeNumber, timeUnit, branchName := branchInfoFromLine(line)
timeUnit = abbreviatedTimeUnit(timeUnit)
branch := &commands.Branch{Name: branchName, Recency: timeNumber + timeUnit}
branch := commands.Branch{Name: branchName, Recency: timeNumber + timeUnit}
branches = append(branches, branch)
}
return uniqueByName(branches)
return branches
}
func (b *BranchListBuilder) obtainSafeBranches() []*commands.Branch {
branches := make([]*commands.Branch, 0)
func (b *BranchListBuilder) obtainSafeBranches() []commands.Branch {
branches := make([]commands.Branch, 0)
bIter, err := b.GitCommand.Repo.Branches()
if err != nil {
panic(err)
}
bIter.ForEach(func(b *plumbing.Reference) error {
err = bIter.ForEach(func(b *plumbing.Reference) error {
name := b.Name().Short()
branches = append(branches, &commands.Branch{Name: name})
branches = append(branches, commands.Branch{Name: name})
return nil
})
return branches
}
func (b *BranchListBuilder) appendNewBranches(finalBranches, newBranches, existingBranches []*commands.Branch, included bool) []*commands.Branch {
func (b *BranchListBuilder) appendNewBranches(finalBranches, newBranches, existingBranches []commands.Branch, included bool) []commands.Branch {
for _, newBranch := range newBranches {
if included == branchIncluded(newBranch.Name, existingBranches) {
finalBranches = append(finalBranches, newBranch)
@@ -88,7 +89,7 @@ func (b *BranchListBuilder) appendNewBranches(finalBranches, newBranches, existi
return finalBranches
}
func sanitisedReflogName(reflogBranch *commands.Branch, safeBranches []*commands.Branch) string {
func sanitisedReflogName(reflogBranch commands.Branch, safeBranches []commands.Branch) string {
for _, safeBranch := range safeBranches {
if strings.ToLower(safeBranch.Name) == strings.ToLower(reflogBranch.Name) {
return safeBranch.Name
@@ -98,12 +99,15 @@ func sanitisedReflogName(reflogBranch *commands.Branch, safeBranches []*commands
}
// Build the list of branches for the current repo
func (b *BranchListBuilder) Build() []*commands.Branch {
branches := make([]*commands.Branch, 0)
func (b *BranchListBuilder) Build() []commands.Branch {
branches := make([]commands.Branch, 0)
head := b.obtainCurrentBranch()
safeBranches := b.obtainSafeBranches()
if len(safeBranches) == 0 {
return append(branches, head)
}
reflogBranches := b.obtainReflogBranches()
reflogBranches = uniqueByName(append([]commands.Branch{head}, reflogBranches...))
for i, reflogBranch := range reflogBranches {
reflogBranches[i].Name = sanitisedReflogName(reflogBranch, safeBranches)
}
@@ -111,16 +115,10 @@ func (b *BranchListBuilder) Build() []*commands.Branch {
branches = b.appendNewBranches(branches, reflogBranches, safeBranches, true)
branches = b.appendNewBranches(branches, safeBranches, branches, false)
if len(branches) == 0 || branches[0].Name != head.Name {
branches = append([]*commands.Branch{head}, branches...)
}
branches[0].Recency = " *"
return branches
}
func branchIncluded(branchName string, branches []*commands.Branch) bool {
func branchIncluded(branchName string, branches []commands.Branch) bool {
for _, existingBranch := range branches {
if strings.ToLower(existingBranch.Name) == strings.ToLower(branchName) {
return true
@@ -129,8 +127,8 @@ func branchIncluded(branchName string, branches []*commands.Branch) bool {
return false
}
func uniqueByName(branches []*commands.Branch) []*commands.Branch {
finalBranches := make([]*commands.Branch, 0)
func uniqueByName(branches []commands.Branch) []commands.Branch {
finalBranches := make([]commands.Branch, 0)
for _, branch := range branches {
if branchIncluded(branch.Name, finalBranches) {
continue
-296
View File
@@ -1,296 +0,0 @@
package git
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
// context:
// here we get the commits from git log but format them to show whether they're
// unpushed/pushed/merged into the base branch or not, or if they're yet to
// be processed as part of a rebase (these won't appear in git log but we
// grab them from the rebase-related files in the .git directory to show them
// if we find out we need to use one of these functions in the git.go file, we
// can just pull them out of here and put them there and then call them from in here
// CommitListBuilder returns a list of Branch objects for the current repo
type CommitListBuilder struct {
Log *logrus.Entry
GitCommand *commands.GitCommand
OSCommand *commands.OSCommand
Tr *i18n.Localizer
CherryPickedCommits []*commands.Commit
DiffEntries []*commands.Commit
}
// NewCommitListBuilder builds a new commit list builder
func NewCommitListBuilder(log *logrus.Entry, gitCommand *commands.GitCommand, osCommand *commands.OSCommand, tr *i18n.Localizer, cherryPickedCommits []*commands.Commit, diffEntries []*commands.Commit) (*CommitListBuilder, error) {
return &CommitListBuilder{
Log: log,
GitCommand: gitCommand,
OSCommand: osCommand,
Tr: tr,
CherryPickedCommits: cherryPickedCommits,
DiffEntries: diffEntries,
}, nil
}
// GetCommits obtains the commits of the current branch
func (c *CommitListBuilder) GetCommits() ([]*commands.Commit, error) {
commits := []*commands.Commit{}
var rebasingCommits []*commands.Commit
rebaseMode, err := c.GitCommand.RebaseMode()
if err != nil {
return nil, err
}
if rebaseMode != "" {
// here we want to also prepend the commits that we're in the process of rebasing
rebasingCommits, err = c.getRebasingCommits(rebaseMode)
if err != nil {
return nil, err
}
if len(rebasingCommits) > 0 {
commits = append(commits, rebasingCommits...)
}
}
unpushedCommits := c.getUnpushedCommits()
log := c.getLog()
// now we can split it up and turn it into commits
for _, line := range utils.SplitLines(log) {
splitLine := strings.Split(line, " ")
sha := splitLine[0]
_, unpushed := unpushedCommits[sha]
status := map[bool]string{true: "unpushed", false: "pushed"}[unpushed]
commits = append(commits, &commands.Commit{
Sha: sha,
Name: strings.Join(splitLine[1:], " "),
Status: status,
DisplayString: strings.Join(splitLine, " "),
})
}
if rebaseMode != "" {
currentCommit := commits[len(rebasingCommits)]
blue := color.New(color.FgYellow)
youAreHere := blue.Sprintf("<-- %s ---", c.Tr.SLocalize("YouAreHere"))
currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name)
}
commits, err = c.setCommitMergedStatuses(commits)
if err != nil {
return nil, err
}
commits, err = c.setCommitCherryPickStatuses(commits)
if err != nil {
return nil, err
}
for _, commit := range commits {
for _, entry := range c.DiffEntries {
if entry.Sha == commit.Sha {
commit.Status = "selected"
}
}
}
return commits, nil
}
// getRebasingCommits obtains the commits that we're in the process of rebasing
func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*commands.Commit, error) {
switch rebaseMode {
case "normal":
return c.getNormalRebasingCommits()
case "interactive":
return c.getInteractiveRebasingCommits()
default:
return nil, nil
}
}
func (c *CommitListBuilder) getNormalRebasingCommits() ([]*commands.Commit, error) {
rewrittenCount := 0
bytesContent, err := ioutil.ReadFile(fmt.Sprintf("%s/rebase-apply/rewritten", c.GitCommand.DotGitDir))
if err == nil {
content := string(bytesContent)
rewrittenCount = len(strings.Split(content, "\n"))
}
// we know we're rebasing, so lets get all the files whose names have numbers
commits := []*commands.Commit{}
err = filepath.Walk(fmt.Sprintf("%s/rebase-apply", c.GitCommand.DotGitDir), func(path string, f os.FileInfo, err error) error {
if rewrittenCount > 0 {
rewrittenCount--
return nil
}
if err != nil {
return err
}
re := regexp.MustCompile(`^\d+$`)
if !re.MatchString(f.Name()) {
return nil
}
bytesContent, err := ioutil.ReadFile(path)
if err != nil {
return err
}
content := string(bytesContent)
commit, err := c.commitFromPatch(content)
if err != nil {
return err
}
commits = append([]*commands.Commit{commit}, commits...)
return nil
})
if err != nil {
return nil, err
}
return commits, nil
}
// git-rebase-todo example:
// pick ac446ae94ee560bdb8d1d057278657b251aaef17 ac446ae
// pick afb893148791a2fbd8091aeb81deba4930c73031 afb8931
// git-rebase-todo.backup example:
// pick 49cbba374296938ea86bbd4bf4fee2f6ba5cccf6 third commit on master
// pick ac446ae94ee560bdb8d1d057278657b251aaef17 blah commit on master
// pick afb893148791a2fbd8091aeb81deba4930c73031 fourth commit on master
// getInteractiveRebasingCommits takes our git-rebase-todo and our git-rebase-todo.backup files
// and extracts out the sha and names of commits that we still have to go
// in the rebase:
func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*commands.Commit, error) {
bytesContent, err := ioutil.ReadFile(fmt.Sprintf("%s/rebase-merge/git-rebase-todo", c.GitCommand.DotGitDir))
if err != nil {
c.Log.Info(fmt.Sprintf("error occurred reading git-rebase-todo: %s", err.Error()))
// we assume an error means the file doesn't exist so we just return
return nil, nil
}
commits := []*commands.Commit{}
lines := strings.Split(string(bytesContent), "\n")
for _, line := range lines {
if line == "" || line == "noop" {
return commits, nil
}
splitLine := strings.Split(line, " ")
commits = append([]*commands.Commit{{
Sha: splitLine[1][0:7],
Name: strings.Join(splitLine[2:], " "),
Status: "rebasing",
Action: splitLine[0],
}}, commits...)
}
return nil, nil
}
// assuming the file starts like this:
// From e93d4193e6dd45ca9cf3a5a273d7ba6cd8b8fb20 Mon Sep 17 00:00:00 2001
// From: Lazygit Tester <test@example.com>
// Date: Wed, 5 Dec 2018 21:03:23 +1100
// Subject: second commit on master
func (c *CommitListBuilder) commitFromPatch(content string) (*commands.Commit, error) {
lines := strings.Split(content, "\n")
sha := strings.Split(lines[0], " ")[1][0:7]
name := strings.TrimPrefix(lines[3], "Subject: ")
return &commands.Commit{
Sha: sha,
Name: name,
Status: "rebasing",
}, nil
}
func (c *CommitListBuilder) setCommitMergedStatuses(commits []*commands.Commit) ([]*commands.Commit, error) {
ancestor, err := c.getMergeBase()
if err != nil {
return nil, err
}
if ancestor == "" {
return commits, nil
}
passedAncestor := false
for i, commit := range commits {
if strings.HasPrefix(ancestor, commit.Sha) {
passedAncestor = true
}
if commit.Status != "pushed" {
continue
}
if passedAncestor {
commits[i].Status = "merged"
}
}
return commits, nil
}
func (c *CommitListBuilder) setCommitCherryPickStatuses(commits []*commands.Commit) ([]*commands.Commit, error) {
for _, commit := range commits {
for _, cherryPickedCommit := range c.CherryPickedCommits {
if commit.Sha == cherryPickedCommit.Sha {
commit.Copied = true
}
}
}
return commits, nil
}
func (c *CommitListBuilder) getMergeBase() (string, error) {
currentBranch, err := c.GitCommand.CurrentBranchName()
if err != nil {
return "", err
}
baseBranch := "master"
if strings.HasPrefix(currentBranch, "feature/") {
baseBranch = "develop"
}
// swallowing error because it's not a big deal; probably because there are no commits yet
output, _ := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git merge-base HEAD %s", baseBranch))
return output, nil
}
// getUnpushedCommits Returns the sha's of the commits that have not yet been pushed
// to the remote branch of the current branch, a map is returned to ease look up
func (c *CommitListBuilder) getUnpushedCommits() map[string]bool {
pushables := map[string]bool{}
o, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..HEAD --abbrev-commit")
if err != nil {
return pushables
}
for _, p := range utils.SplitLines(o) {
pushables[p] = true
}
return pushables
}
// getLog gets the git log (currently limited to 30 commits for performance
// until we work out lazy loading
func (c *CommitListBuilder) getLog() string {
// currently limiting to 30 for performance reasons
// TODO: add lazyloading when you scroll down
result, err := c.OSCommand.RunCommandWithOutput("git log --oneline -30")
if err != nil {
// assume if there is an error there are no commits yet for this branch
return ""
}
return result
}
-315
View File
@@ -1,315 +0,0 @@
package git
import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/stretchr/testify/assert"
)
// NewDummyCommitListBuilder creates a new dummy CommitListBuilder for testing
func NewDummyCommitListBuilder() *CommitListBuilder {
osCommand := commands.NewDummyOSCommand()
return &CommitListBuilder{
Log: commands.NewDummyLog(),
GitCommand: commands.NewDummyGitCommandWithOSCommand(osCommand),
OSCommand: osCommand,
Tr: i18n.NewLocalizer(commands.NewDummyLog()),
CherryPickedCommits: []*commands.Commit{},
}
}
// TestCommitListBuilderGetUnpushedCommits is a function.
func TestCommitListBuilderGetUnpushedCommits(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(map[string]bool)
}
scenarios := []scenario{
{
"Can't retrieve pushable commits",
func(string, ...string) *exec.Cmd {
return exec.Command("test")
},
func(pushables map[string]bool) {
assert.EqualValues(t, map[string]bool{}, pushables)
},
},
{
"Retrieve pushable commits",
func(cmd string, args ...string) *exec.Cmd {
return exec.Command("echo", "8a2bb0e\n78976bc")
},
func(pushables map[string]bool) {
assert.Len(t, pushables, 2)
assert.EqualValues(t, map[string]bool{"8a2bb0e": true, "78976bc": true}, pushables)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.getUnpushedCommits())
})
}
}
// TestCommitListBuilderGetMergeBase is a function.
func TestCommitListBuilderGetMergeBase(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(string, error)
}
scenarios := []scenario{
{
"swallows an error if the call to merge-base returns an error",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("test")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "", output)
},
},
{
"returns the commit when master",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("echo", "blah")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.Equal(t, "blah\n", output)
},
},
{
"checks against develop when a feature branch",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "feature/test")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "develop"}, args)
return exec.Command("echo", "blah")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.Equal(t, "blah\n", output)
},
},
{
"bubbles up error if there is one",
func(cmd string, args ...string) *exec.Cmd {
return exec.Command("test")
},
func(output string, err error) {
assert.Error(t, err)
assert.Equal(t, "", output)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.getMergeBase())
})
}
}
// TestCommitListBuilderGetLog is a function.
func TestCommitListBuilderGetLog(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(string)
}
scenarios := []scenario{
{
"Retrieves logs",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
return exec.Command("echo", "6f0b32f commands/git : add GetCommits tests refactor\n9d9d775 circle : remove new line")
},
func(output string) {
assert.EqualValues(t, "6f0b32f commands/git : add GetCommits tests refactor\n9d9d775 circle : remove new line\n", output)
},
},
{
"An error occurred when retrieving logs",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
return exec.Command("test")
},
func(output string) {
assert.Empty(t, output)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.getLog())
})
}
}
// TestCommitListBuilderGetCommits is a function.
func TestCommitListBuilderGetCommits(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func([]*commands.Commit, error)
}
scenarios := []scenario{
{
"No data found",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "rev-list":
assert.EqualValues(t, []string{"rev-list", "@{u}..HEAD", "--abbrev-commit"}, args)
return exec.Command("echo")
case "log":
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
return exec.Command("echo")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("test")
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
}
return nil
},
func(commits []*commands.Commit, err error) {
assert.NoError(t, err)
assert.Len(t, commits, 0)
},
},
{
"GetCommits returns 2 commits, 1 unpushed, the other merged",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "rev-list":
assert.EqualValues(t, []string{"rev-list", "@{u}..HEAD", "--abbrev-commit"}, args)
return exec.Command("echo", "8a2bb0e")
case "log":
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
return exec.Command("echo", "8a2bb0e commit 1\n78976bc commit 2")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("echo", "78976bc")
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
}
return nil
},
func(commits []*commands.Commit, err error) {
assert.NoError(t, err)
assert.Len(t, commits, 2)
assert.EqualValues(t, []*commands.Commit{
{
Sha: "8a2bb0e",
Name: "commit 1",
Status: "unpushed",
DisplayString: "8a2bb0e commit 1",
},
{
Sha: "78976bc",
Name: "commit 2",
Status: "merged",
DisplayString: "78976bc commit 2",
},
}, commits)
},
},
{
"GetCommits bubbles up an error from setCommitMergedStatuses",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "rev-list":
assert.EqualValues(t, []string{"rev-list", "@{u}..HEAD", "--abbrev-commit"}, args)
return exec.Command("echo", "8a2bb0e")
case "log":
assert.EqualValues(t, []string{"log", "--oneline", "-30"}, args)
return exec.Command("echo", "8a2bb0e commit 1\n78976bc commit 2")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("echo", "78976bc")
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
// here's where we are returning the error
return exec.Command("test")
case "rev-parse":
assert.EqualValues(t, []string{"rev-parse", "--short", "HEAD"}, args)
// here too
return exec.Command("test")
}
return nil
},
func(commits []*commands.Commit, err error) {
assert.Error(t, err)
assert.Len(t, commits, 0)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.GetCommits())
})
}
}
-157
View File
@@ -1,157 +0,0 @@
package git
import (
"regexp"
"strconv"
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
type PatchModifier struct {
Log *logrus.Entry
Tr *i18n.Localizer
}
// NewPatchModifier builds a new branch list builder
func NewPatchModifier(log *logrus.Entry) (*PatchModifier, error) {
return &PatchModifier{
Log: log,
}, nil
}
// ModifyPatchForHunk takes the original patch, which may contain several hunks,
// and removes any hunks that aren't the selected hunk
func (p *PatchModifier) ModifyPatchForHunk(patch string, hunkStarts []int, currentLine int) (string, error) {
// get hunk start and end
lines := strings.Split(patch, "\n")
hunkStartIndex := utils.PrevIndex(hunkStarts, currentLine)
hunkStart := hunkStarts[hunkStartIndex]
nextHunkStartIndex := utils.NextIndex(hunkStarts, currentLine)
var hunkEnd int
if nextHunkStartIndex == 0 {
hunkEnd = len(lines) - 1
} else {
hunkEnd = hunkStarts[nextHunkStartIndex]
}
headerLength, err := p.getHeaderLength(lines)
if err != nil {
return "", err
}
output := strings.Join(lines[0:headerLength], "\n") + "\n"
output += strings.Join(lines[hunkStart:hunkEnd], "\n") + "\n"
return output, nil
}
func (p *PatchModifier) getHeaderLength(patchLines []string) (int, error) {
for index, line := range patchLines {
if strings.HasPrefix(line, "@@") {
return index, nil
}
}
return 0, errors.New(p.Tr.SLocalize("CantFindHunks"))
}
// ModifyPatchForLine takes the original patch, which may contain several hunks,
// and the line number of the line we want to stage
func (p *PatchModifier) ModifyPatchForLine(patch string, lineNumber int) (string, error) {
lines := strings.Split(patch, "\n")
headerLength, err := p.getHeaderLength(lines)
if err != nil {
return "", err
}
output := strings.Join(lines[0:headerLength], "\n") + "\n"
hunkStart, err := p.getHunkStart(lines, lineNumber)
if err != nil {
return "", err
}
hunk, err := p.getModifiedHunk(lines, hunkStart, lineNumber)
if err != nil {
return "", err
}
output += strings.Join(hunk, "\n")
return output, nil
}
// getHunkStart returns the line number of the hunk we're going to be modifying
// in order to stage our line
func (p *PatchModifier) getHunkStart(patchLines []string, lineNumber int) (int, error) {
// find the hunk that we're modifying
hunkStart := 0
for index, line := range patchLines {
if strings.HasPrefix(line, "@@") {
hunkStart = index
}
if index == lineNumber {
return hunkStart, nil
}
}
return 0, errors.New(p.Tr.SLocalize("CantFindHunk"))
}
func (p *PatchModifier) getModifiedHunk(patchLines []string, hunkStart int, lineNumber int) ([]string, error) {
lineChanges := 0
// strip the hunk down to just the line we want to stage
newHunk := []string{patchLines[hunkStart]}
for offsetIndex, line := range patchLines[hunkStart+1:] {
index := offsetIndex + hunkStart + 1
if strings.HasPrefix(line, "@@") {
newHunk = append(newHunk, "\n")
break
}
if index != lineNumber {
// we include other removals but treat them like context
if strings.HasPrefix(line, "-") {
newHunk = append(newHunk, " "+line[1:])
lineChanges += 1
continue
}
// we don't include other additions
if strings.HasPrefix(line, "+") {
lineChanges -= 1
continue
}
}
newHunk = append(newHunk, line)
}
var err error
newHunk[0], err = p.updatedHeader(newHunk[0], lineChanges)
if err != nil {
return nil, err
}
return newHunk, nil
}
// updatedHeader returns the hunk header with the updated line range
// we need to update the hunk length to reflect the changes we made
// if the hunk has three additions but we're only staging one, then
// @@ -14,8 +14,11 @@ import (
// becomes
// @@ -14,8 +14,9 @@ import (
func (p *PatchModifier) updatedHeader(currentHeader string, lineChanges int) (string, error) {
// current counter is the number after the second comma
re := regexp.MustCompile(`(\d+) @@`)
prevLengthString := re.FindStringSubmatch(currentHeader)[1]
prevLength, err := strconv.Atoi(prevLengthString)
if err != nil {
return "", err
}
re = regexp.MustCompile(`\d+ @@`)
newLength := strconv.Itoa(prevLength + lineChanges)
return re.ReplaceAllString(currentHeader, newLength+" @@"), nil
}
-85
View File
@@ -1,85 +0,0 @@
package git
import (
"io/ioutil"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/stretchr/testify/assert"
)
// NewDummyPatchModifier constructs a new dummy patch modifier for testing
func NewDummyPatchModifier() *PatchModifier {
return &PatchModifier{
Log: commands.NewDummyLog(),
}
}
func TestModifyPatchForLine(t *testing.T) {
type scenario struct {
testName string
patchFilename string
lineNumber int
shouldError bool
expectedPatchFilename string
}
scenarios := []scenario{
{
"Removing one line",
"testdata/testPatchBefore.diff",
8,
false,
"testdata/testPatchAfter1.diff",
},
{
"Adding one line",
"testdata/testPatchBefore.diff",
10,
false,
"testdata/testPatchAfter2.diff",
},
{
"Adding one line in top hunk in diff with multiple hunks",
"testdata/testPatchBefore2.diff",
20,
false,
"testdata/testPatchAfter3.diff",
},
{
"Adding one line in top hunk in diff with multiple hunks",
"testdata/testPatchBefore2.diff",
53,
false,
"testdata/testPatchAfter4.diff",
},
{
"adding unstaged file with a single line",
"testdata/addedFile.diff",
6,
false,
"testdata/addedFile.diff",
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
p := NewDummyPatchModifier()
beforePatch, err := ioutil.ReadFile(s.patchFilename)
if err != nil {
panic("Cannot open file at " + s.patchFilename)
}
afterPatch, err := p.ModifyPatchForLine(string(beforePatch), s.lineNumber)
if s.shouldError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
expected, err := ioutil.ReadFile(s.expectedPatchFilename)
if err != nil {
panic("Cannot open file at " + s.expectedPatchFilename)
}
assert.Equal(t, string(expected), afterPatch)
}
})
}
}
-36
View File
@@ -1,36 +0,0 @@
package git
import (
"strings"
"github.com/sirupsen/logrus"
)
type PatchParser struct {
Log *logrus.Entry
}
// NewPatchParser builds a new branch list builder
func NewPatchParser(log *logrus.Entry) (*PatchParser, error) {
return &PatchParser{
Log: log,
}, nil
}
func (p *PatchParser) ParsePatch(patch string) ([]int, []int, error) {
lines := strings.Split(patch, "\n")
hunkStarts := []int{}
stageableLines := []int{}
pastHeader := false
for index, line := range lines {
if strings.HasPrefix(line, "@@") {
pastHeader = true
hunkStarts = append(hunkStarts, index)
}
if pastHeader && (strings.HasPrefix(line, "-") || strings.HasPrefix(line, "+")) {
stageableLines = append(stageableLines, index)
}
}
p.Log.WithField("staging", "staging").Info(stageableLines)
return hunkStarts, stageableLines, nil
}
-68
View File
@@ -1,68 +0,0 @@
package git
import (
"io/ioutil"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/stretchr/testify/assert"
)
// NewDummyPatchParser constructs a new dummy patch parser for testing
func NewDummyPatchParser() *PatchParser {
return &PatchParser{
Log: commands.NewDummyLog(),
}
}
func TestParsePatch(t *testing.T) {
type scenario struct {
testName string
patchFilename string
shouldError bool
expectedStageableLines []int
expectedHunkStarts []int
}
scenarios := []scenario{
{
"Diff with one hunk",
"testdata/testPatchBefore.diff",
false,
[]int{8, 9, 10, 11},
[]int{4},
},
{
"Diff with two hunks",
"testdata/testPatchBefore2.diff",
false,
[]int{8, 9, 10, 11, 12, 13, 20, 21, 22, 23, 24, 25, 26, 27, 28, 33, 34, 35, 36, 37, 45, 46, 47, 48, 49, 50, 51, 52, 53},
[]int{4, 41},
},
{
"Unstaged file",
"testdata/addedFile.diff",
false,
[]int{6},
[]int{5},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
p := NewDummyPatchParser()
beforePatch, err := ioutil.ReadFile(s.patchFilename)
if err != nil {
panic("Cannot open file at " + s.patchFilename)
}
hunkStarts, stageableLines, err := p.ParsePatch(string(beforePatch))
if s.shouldError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, s.expectedStageableLines, stageableLines)
assert.Equal(t, s.expectedHunkStarts, hunkStarts)
}
})
}
}
-7
View File
@@ -1,7 +0,0 @@
diff --git a/blah b/blah
new file mode 100644
index 0000000..907b308
--- /dev/null
+++ b/blah
@@ -0,0 +1 @@
+blah
-13
View File
@@ -1,13 +0,0 @@
diff --git a/pkg/git/branch_list_builder.go b/pkg/git/branch_list_builder.go
index 60ec4e0..db4485d 100644
--- a/pkg/git/branch_list_builder.go
+++ b/pkg/git/branch_list_builder.go
@@ -14,8 +14,7 @@ import (
// context:
// we want to only show 'safe' branches (ones that haven't e.g. been deleted)
-// which `git branch -a` gives us, but we also want the recency data that
// git reflog gives us.
// So we get the HEAD, then append get the reflog branches that intersect with
// our safe branches, then add the remaining safe branches, ensuring uniqueness
// along the way
-14
View File
@@ -1,14 +0,0 @@
diff --git a/pkg/git/branch_list_builder.go b/pkg/git/branch_list_builder.go
index 60ec4e0..db4485d 100644
--- a/pkg/git/branch_list_builder.go
+++ b/pkg/git/branch_list_builder.go
@@ -14,8 +14,9 @@ import (
// context:
// we want to only show 'safe' branches (ones that haven't e.g. been deleted)
// which `git branch -a` gives us, but we also want the recency data that
// git reflog gives us.
+// test 2 - if I remove this, I decrement the end counter
// So we get the HEAD, then append get the reflog branches that intersect with
// our safe branches, then add the remaining safe branches, ensuring uniqueness
// along the way
-25
View File
@@ -1,25 +0,0 @@
diff --git a/pkg/git/patch_modifier.go b/pkg/git/patch_modifier.go
index a8fc600..6d8f7d7 100644
--- a/pkg/git/patch_modifier.go
+++ b/pkg/git/patch_modifier.go
@@ -36,18 +36,19 @@ func (p *PatchModifier) ModifyPatchForHunk(patch string, hunkStarts []int, curre
hunkEnd = hunkStarts[nextHunkStartIndex]
}
headerLength := 4
output := strings.Join(lines[0:headerLength], "\n") + "\n"
output += strings.Join(lines[hunkStart:hunkEnd], "\n") + "\n"
return output, nil
}
+func getHeaderLength(patchLines []string) (int, error) {
// ModifyPatchForLine takes the original patch, which may contain several hunks,
// and the line number of the line we want to stage
func (p *PatchModifier) ModifyPatchForLine(patch string, lineNumber int) (string, error) {
lines := strings.Split(patch, "\n")
headerLength := 4
output := strings.Join(lines[0:headerLength], "\n") + "\n"
hunkStart, err := p.getHunkStart(lines, lineNumber)
-19
View File
@@ -1,19 +0,0 @@
diff --git a/pkg/git/patch_modifier.go b/pkg/git/patch_modifier.go
index a8fc600..6d8f7d7 100644
--- a/pkg/git/patch_modifier.go
+++ b/pkg/git/patch_modifier.go
@@ -124,13 +140,14 @@ func (p *PatchModifier) getModifiedHunk(patchLines []string, hunkStart int, line
// @@ -14,8 +14,9 @@ import (
func (p *PatchModifier) updatedHeader(currentHeader string, lineChanges int) (string, error) {
// current counter is the number after the second comma
re := regexp.MustCompile(`^[^,]+,[^,]+,(\d+)`)
matches := re.FindStringSubmatch(currentHeader)
if len(matches) < 2 {
re = regexp.MustCompile(`^[^,]+,[^+]+\+(\d+)`)
matches = re.FindStringSubmatch(currentHeader)
}
prevLengthString := matches[1]
+ prevLengthString := re.FindStringSubmatch(currentHeader)[1]
prevLength, err := strconv.Atoi(prevLengthString)
if err != nil {
-15
View File
@@ -1,15 +0,0 @@
diff --git a/pkg/git/branch_list_builder.go b/pkg/git/branch_list_builder.go
index 60ec4e0..db4485d 100644
--- a/pkg/git/branch_list_builder.go
+++ b/pkg/git/branch_list_builder.go
@@ -14,8 +14,8 @@ import (
// context:
// we want to only show 'safe' branches (ones that haven't e.g. been deleted)
-// which `git branch -a` gives us, but we also want the recency data that
-// git reflog gives us.
+// test 2 - if I remove this, I decrement the end counter
+// test
// So we get the HEAD, then append get the reflog branches that intersect with
// our safe branches, then add the remaining safe branches, ensuring uniqueness
// along the way
-57
View File
@@ -1,57 +0,0 @@
diff --git a/pkg/git/patch_modifier.go b/pkg/git/patch_modifier.go
index a8fc600..6d8f7d7 100644
--- a/pkg/git/patch_modifier.go
+++ b/pkg/git/patch_modifier.go
@@ -36,18 +36,34 @@ func (p *PatchModifier) ModifyPatchForHunk(patch string, hunkStarts []int, curre
hunkEnd = hunkStarts[nextHunkStartIndex]
}
- headerLength := 4
+ headerLength, err := getHeaderLength(lines)
+ if err != nil {
+ return "", err
+ }
+
output := strings.Join(lines[0:headerLength], "\n") + "\n"
output += strings.Join(lines[hunkStart:hunkEnd], "\n") + "\n"
return output, nil
}
+func getHeaderLength(patchLines []string) (int, error) {
+ for index, line := range patchLines {
+ if strings.HasPrefix(line, "@@") {
+ return index, nil
+ }
+ }
+ return 0, errors.New("Could not find any hunks in this patch")
+}
+
// ModifyPatchForLine takes the original patch, which may contain several hunks,
// and the line number of the line we want to stage
func (p *PatchModifier) ModifyPatchForLine(patch string, lineNumber int) (string, error) {
lines := strings.Split(patch, "\n")
- headerLength := 4
+ headerLength, err := getHeaderLength(lines)
+ if err != nil {
+ return "", err
+ }
output := strings.Join(lines[0:headerLength], "\n") + "\n"
hunkStart, err := p.getHunkStart(lines, lineNumber)
@@ -124,13 +140,8 @@ func (p *PatchModifier) getModifiedHunk(patchLines []string, hunkStart int, line
// @@ -14,8 +14,9 @@ import (
func (p *PatchModifier) updatedHeader(currentHeader string, lineChanges int) (string, error) {
// current counter is the number after the second comma
- re := regexp.MustCompile(`^[^,]+,[^,]+,(\d+)`)
- matches := re.FindStringSubmatch(currentHeader)
- if len(matches) < 2 {
- re = regexp.MustCompile(`^[^,]+,[^+]+\+(\d+)`)
- matches = re.FindStringSubmatch(currentHeader)
- }
- prevLengthString := matches[1]
+ re := regexp.MustCompile(`(\d+) @@`)
+ prevLengthString := re.FindStringSubmatch(currentHeader)[1]
prevLength, err := strconv.Atoi(prevLengthString)
if err != nil {
-70
View File
@@ -1,70 +0,0 @@
package gui
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type appStatus struct {
name string
statusType string
duration int
}
type statusManager struct {
statuses []appStatus
}
func (m *statusManager) removeStatus(name string) {
newStatuses := []appStatus{}
for _, status := range m.statuses {
if status.name != name {
newStatuses = append(newStatuses, status)
}
}
m.statuses = newStatuses
}
func (m *statusManager) addWaitingStatus(name string) {
m.removeStatus(name)
newStatus := appStatus{
name: name,
statusType: "waiting",
duration: 0,
}
m.statuses = append([]appStatus{newStatus}, m.statuses...)
}
func (m *statusManager) getStatusString() string {
if len(m.statuses) == 0 {
return ""
}
topStatus := m.statuses[0]
if topStatus.statusType == "waiting" {
return topStatus.name + " " + utils.Loader()
}
return topStatus.name
}
// WithWaitingStatus wraps a function and shows a waiting status while the function is still executing
func (gui *Gui) WithWaitingStatus(name string, f func() error) error {
go func() {
gui.g.Update(func(g *gocui.Gui) error {
gui.statusManager.addWaitingStatus(name)
return nil
})
defer gui.g.Update(func(g *gocui.Gui) error {
gui.statusManager.removeStatus(name)
return nil
})
if err := f(); err != nil {
gui.g.Update(func(g *gocui.Gui) error {
return gui.createErrorPanel(gui.g, err.Error())
})
}
}()
return nil
}
+74 -260
View File
@@ -9,145 +9,20 @@ import (
"github.com/jesseduffield/lazygit/pkg/git"
)
// list panel functions
func (gui *Gui) getSelectedBranch() *commands.Branch {
selectedLine := gui.State.Panels.Branches.SelectedLine
if selectedLine == -1 {
return nil
}
return gui.State.Branches[selectedLine]
}
// may want to standardise how these select methods work
func (gui *Gui) handleBranchSelect(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
// This really shouldn't happen: there should always be a master branch
if len(gui.State.Branches) == 0 {
return gui.renderString(g, "main", gui.Tr.SLocalize("NoBranchesThisRepo"))
}
branch := gui.getSelectedBranch()
if err := gui.focusPoint(0, gui.State.Panels.Branches.SelectedLine, len(gui.State.Branches), v); err != nil {
return err
}
go func() {
_ = gui.RenderSelectedBranchUpstreamDifferences()
}()
go func() {
graph, err := gui.GitCommand.GetBranchGraph(branch.Name)
if err != nil && strings.HasPrefix(graph, "fatal: ambiguous argument") {
graph = gui.Tr.SLocalize("NoTrackingThisBranch")
}
_ = gui.renderString(g, "main", graph)
}()
return nil
}
func (gui *Gui) RenderSelectedBranchUpstreamDifferences() error {
// here we tell the selected branch that it is selected.
// this is necessary for showing stats on a branch that is selected, because
// the displaystring function doesn't have access to gui state to tell if it's selected
for i, branch := range gui.State.Branches {
branch.Selected = i == gui.State.Panels.Branches.SelectedLine
}
branch := gui.getSelectedBranch()
branch.Pushables, branch.Pullables = gui.GitCommand.GetBranchUpstreamDifferenceCount(branch.Name)
return gui.renderListPanel(gui.getBranchesView(), gui.State.Branches)
}
// gui.refreshStatus is called at the end of this because that's when we can
// be sure there is a state.Branches array to pick the current branch from
func (gui *Gui) refreshBranches(g *gocui.Gui) error {
g.Update(func(g *gocui.Gui) error {
builder, err := git.NewBranchListBuilder(gui.Log, gui.GitCommand)
if err != nil {
return err
}
gui.State.Branches = builder.Build()
gui.refreshSelectedLine(&gui.State.Panels.Branches.SelectedLine, len(gui.State.Branches))
if err := gui.RenderSelectedBranchUpstreamDifferences(); err != nil {
return err
}
return gui.refreshStatus(g)
})
return nil
}
func (gui *Gui) handleBranchesNextLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
panelState := gui.State.Panels.Branches
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Branches), false)
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
return gui.handleBranchSelect(gui.g, v)
}
func (gui *Gui) handleBranchesPrevLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
panelState := gui.State.Panels.Branches
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Branches), true)
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
return gui.handleBranchSelect(gui.g, v)
}
// specific functions
func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error {
if gui.State.Panels.Branches.SelectedLine == -1 {
return nil
}
if gui.State.Panels.Branches.SelectedLine == 0 {
index := gui.getItemPosition(v)
if index == 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("AlreadyCheckedOutBranch"))
}
branch := gui.getSelectedBranch()
return gui.handleCheckoutBranch(branch.Name)
}
func (gui *Gui) handleCreatePullRequestPress(g *gocui.Gui, v *gocui.View) error {
pullRequest := commands.NewPullRequest(gui.GitCommand)
branch := gui.getSelectedBranch()
if err := pullRequest.Create(branch); err != nil {
return gui.createErrorPanel(g, err.Error())
branch := gui.getSelectedBranch(v)
if err := gui.GitCommand.Checkout(branch.Name, false); err != nil {
gui.createErrorPanel(g, err.Error())
}
return nil
}
func (gui *Gui) handleGitFetch(g *gocui.Gui, v *gocui.View) error {
if err := gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("FetchWait")); err != nil {
return err
}
go func() {
unamePassOpend, err := gui.fetch(g, v, true)
gui.HandleCredentialsPopup(g, unamePassOpend, err)
}()
return nil
return gui.refreshSidePanels(g)
}
func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
branch := gui.getSelectedBranch()
branch := gui.getSelectedBranch(v)
message := gui.Tr.SLocalize("SureForceCheckout")
title := gui.Tr.SLocalize("ForceCheckoutBranch")
return gui.createConfirmationPanel(g, v, title, message, func(g *gocui.Gui, v *gocui.View) error {
@@ -158,45 +33,12 @@ func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
}, nil)
}
func (gui *Gui) handleCheckoutBranch(branchName string) error {
if err := gui.GitCommand.Checkout(branchName, false); err != nil {
// note, this will only work for english-language git commands. If we force git to use english, and the error isn't this one, then the user will receive an english command they may not understand. I'm not sure what the best solution to this is. Running the command once in english and a second time in the native language is one option
if strings.Contains(err.Error(), "Please commit your changes or stash them before you switch branch") {
// offer to autostash changes
return gui.createConfirmationPanel(gui.g, gui.getBranchesView(), gui.Tr.SLocalize("AutoStashTitle"), gui.Tr.SLocalize("AutoStashPrompt"), func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.StashSave(gui.Tr.SLocalize("StashPrefix") + branchName); err != nil {
return gui.createErrorPanel(g, err.Error())
}
if err := gui.GitCommand.Checkout(branchName, false); err != nil {
return gui.createErrorPanel(g, err.Error())
}
// checkout successful so we select the new branch
gui.State.Panels.Branches.SelectedLine = 0
if err := gui.GitCommand.StashDo(0, "pop"); err != nil {
if err := gui.refreshSidePanels(g); err != nil {
return err
}
return gui.createErrorPanel(g, err.Error())
}
return gui.refreshSidePanels(g)
}, nil)
}
if err := gui.createErrorPanel(gui.g, err.Error()); err != nil {
return err
}
}
gui.State.Panels.Branches.SelectedLine = 0
return gui.refreshSidePanels(gui.g)
}
func (gui *Gui) handleCheckoutByName(g *gocui.Gui, v *gocui.View) error {
gui.createPromptPanel(g, v, gui.Tr.SLocalize("BranchName")+":", func(g *gocui.Gui, v *gocui.View) error {
return gui.handleCheckoutBranch(gui.trimmedContent(v))
if err := gui.GitCommand.Checkout(gui.trimmedContent(v), false); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return gui.refreshSidePanels(g)
})
return nil
}
@@ -220,123 +62,95 @@ func (gui *Gui) handleNewBranch(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleDeleteBranch(g *gocui.Gui, v *gocui.View) error {
return gui.deleteBranch(g, v, false)
}
func (gui *Gui) handleForceDeleteBranch(g *gocui.Gui, v *gocui.View) error {
return gui.deleteBranch(g, v, true)
}
func (gui *Gui) deleteBranch(g *gocui.Gui, v *gocui.View, force bool) error {
selectedBranch := gui.getSelectedBranch()
if selectedBranch == nil {
return nil
}
checkedOutBranch := gui.State.Branches[0]
selectedBranch := gui.getSelectedBranch(v)
if checkedOutBranch.Name == selectedBranch.Name {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantDeleteCheckOutBranch"))
}
return gui.deleteNamedBranch(g, v, selectedBranch, force)
}
func (gui *Gui) deleteNamedBranch(g *gocui.Gui, v *gocui.View, selectedBranch *commands.Branch, force bool) error {
title := gui.Tr.SLocalize("DeleteBranch")
var messageID string
if force {
messageID = "ForceDeleteBranchMessage"
} else {
messageID = "DeleteBranchMessage"
}
message := gui.Tr.TemplateLocalize(
messageID,
"DeleteBranchMessage",
Teml{
"selectedBranchName": selectedBranch.Name,
},
)
title := gui.Tr.SLocalize("DeleteBranch")
return gui.createConfirmationPanel(g, v, title, message, func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.DeleteBranch(selectedBranch.Name, force); err != nil {
errMessage := err.Error()
if !force && strings.Contains(errMessage, "is not fully merged") {
return gui.deleteNamedBranch(g, v, selectedBranch, true)
}
return gui.createErrorPanel(g, errMessage)
if err := gui.GitCommand.DeleteBranch(selectedBranch.Name); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return gui.refreshSidePanels(g)
}, nil)
}
func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error {
checkedOutBranch := gui.State.Branches[0].Name
selectedBranch := gui.getSelectedBranch().Name
if checkedOutBranch == selectedBranch {
checkedOutBranch := gui.State.Branches[0]
selectedBranch := gui.getSelectedBranch(v)
defer gui.refreshSidePanels(g)
if checkedOutBranch.Name == selectedBranch.Name {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantMergeBranchIntoItself"))
}
prompt := gui.Tr.TemplateLocalize(
"ConfirmMerge",
Teml{
"checkedOutBranch": checkedOutBranch,
"selectedBranch": selectedBranch,
},
)
return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("MergingTitle"), prompt,
func(g *gocui.Gui, v *gocui.View) error {
err := gui.GitCommand.Merge(selectedBranch)
return gui.handleGenericMergeCommandResult(err)
}, nil)
if err := gui.GitCommand.Merge(selectedBranch.Name); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return nil
}
func (gui *Gui) handleRebase(g *gocui.Gui, v *gocui.View) error {
checkedOutBranch := gui.State.Branches[0].Name
selectedBranch := gui.getSelectedBranch().Name
if selectedBranch == checkedOutBranch {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantRebaseOntoSelf"))
}
prompt := gui.Tr.TemplateLocalize(
"ConfirmRebase",
Teml{
"checkedOutBranch": checkedOutBranch,
"selectedBranch": selectedBranch,
},
)
return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("RebasingTitle"), prompt,
func(g *gocui.Gui, v *gocui.View) error {
err := gui.GitCommand.RebaseBranch(selectedBranch)
return gui.handleGenericMergeCommandResult(err)
}, nil)
func (gui *Gui) getSelectedBranch(v *gocui.View) commands.Branch {
lineNumber := gui.getItemPosition(v)
return gui.State.Branches[lineNumber]
}
func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
branch := gui.getSelectedBranch()
if branch == nil {
return nil
func (gui *Gui) renderBranchesOptions(g *gocui.Gui) error {
return gui.renderOptionsMap(g, map[string]string{
"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"),
"← → ↑ ↓": gui.Tr.SLocalize("navigate"),
})
}
// may want to standardise how these select methods work
func (gui *Gui) handleBranchSelect(g *gocui.Gui, v *gocui.View) error {
if err := gui.renderBranchesOptions(g); err != nil {
return err
}
if branch.Pushables == "" {
return nil
// This really shouldn't happen: there should always be a master branch
if len(gui.State.Branches) == 0 {
return gui.renderString(g, "main", gui.Tr.SLocalize("NoBranchesThisRepo"))
}
if branch.Pushables == "?" {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("FwdNoUpstream"))
}
if branch.Pushables != "0" {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("FwdCommitsToPush"))
}
upstream := "origin" // hardcoding for now
message := gui.Tr.TemplateLocalize(
"Fetching",
Teml{
"from": fmt.Sprintf("%s/%s", upstream, branch.Name),
"to": branch.Name,
},
)
go func() {
_ = gui.createLoaderPanel(gui.g, v, message)
if err := gui.GitCommand.FastForward(branch.Name); err != nil {
_ = gui.createErrorPanel(gui.g, err.Error())
} else {
_ = gui.closeConfirmationPrompt(gui.g)
_ = gui.RenderSelectedBranchUpstreamDifferences()
branch := gui.getSelectedBranch(v)
diff, err := gui.GitCommand.GetBranchGraph(branch.Name)
if err != nil && strings.HasPrefix(diff, "fatal: ambiguous argument") {
diff = gui.Tr.SLocalize("NoTrackingThisBranch")
}
gui.renderString(g, "main", diff)
}()
return nil
}
// gui.refreshStatus is called at the end of this because that's when we can
// be sure there is a state.Branches array to pick the current branch from
func (gui *Gui) refreshBranches(g *gocui.Gui) error {
g.Update(func(g *gocui.Gui) error {
v, err := g.View("branches")
if err != nil {
panic(err)
}
builder, err := git.NewBranchListBuilder(gui.Log, gui.GitCommand)
if err != nil {
return err
}
gui.State.Branches = builder.Build()
v.Clear()
for _, branch := range gui.State.Branches {
fmt.Fprintln(v, branch.GetDisplayString())
}
gui.resetOrigin(v)
return gui.refreshStatus(g)
})
return nil
}
-106
View File
@@ -1,106 +0,0 @@
package gui
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
)
func (gui *Gui) getSelectedCommitFile(g *gocui.Gui) *commands.CommitFile {
selectedLine := gui.State.Panels.CommitFiles.SelectedLine
if selectedLine == -1 {
return nil
}
return gui.State.CommitFiles[selectedLine]
}
func (gui *Gui) handleCommitFileSelect(g *gocui.Gui, v *gocui.View) error {
commitFile := gui.getSelectedCommitFile(g)
if commitFile == nil {
return gui.renderString(g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
}
if err := gui.focusPoint(0, gui.State.Panels.CommitFiles.SelectedLine, len(gui.State.CommitFiles), v); err != nil {
return err
}
commitText, err := gui.GitCommand.ShowCommitFile(commitFile.Sha, commitFile.Name)
if err != nil {
return err
}
return gui.renderString(g, "main", commitText)
}
func (gui *Gui) handleCommitFilesNextLine(g *gocui.Gui, v *gocui.View) error {
panelState := gui.State.Panels.CommitFiles
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.CommitFiles), false)
return gui.handleCommitFileSelect(gui.g, v)
}
func (gui *Gui) handleCommitFilesPrevLine(g *gocui.Gui, v *gocui.View) error {
panelState := gui.State.Panels.CommitFiles
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.CommitFiles), true)
return gui.handleCommitFileSelect(gui.g, v)
}
func (gui *Gui) handleSwitchToCommitsPanel(g *gocui.Gui, v *gocui.View) error {
commitsView, err := g.View("commits")
if err != nil {
return err
}
return gui.switchFocus(g, v, commitsView)
}
func (gui *Gui) handleCheckoutCommitFile(g *gocui.Gui, v *gocui.View) error {
file := gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLine]
if err := gui.GitCommand.CheckoutFile(file.Sha, file.Name); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshFiles()
}
func (gui *Gui) handleDiscardOldFileChange(g *gocui.Gui, v *gocui.View) error {
fileName := gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLine].Name
return gui.createConfirmationPanel(gui.g, v, gui.Tr.SLocalize("DiscardFileChangesTitle"), gui.Tr.SLocalize("DiscardFileChangesPrompt"), func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
if err := gui.GitCommand.DiscardOldFileChanges(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, fileName); err != nil {
if err := gui.handleGenericMergeCommandResult(err); err != nil {
return err
}
}
return gui.refreshSidePanels(gui.g)
})
}, nil)
}
func (gui *Gui) refreshCommitFilesView() error {
commit := gui.getSelectedCommit(gui.g)
if commit == nil {
return nil
}
files, err := gui.GitCommand.GetCommitFiles(commit.Sha)
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
gui.State.CommitFiles = files
gui.refreshSelectedLine(&gui.State.Panels.CommitFiles.SelectedLine, len(gui.State.CommitFiles))
if err := gui.renderListPanel(gui.getCommitFilesView(), gui.State.CommitFiles); err != nil {
return err
}
return gui.handleCommitFileSelect(gui.g, gui.getCommitFilesView())
}
func (gui *Gui) handleOpenOldCommitFile(g *gocui.Gui, v *gocui.View) error {
file := gui.getSelectedCommitFile(g)
return gui.openFile(file.Name)
}
+28 -54
View File
@@ -1,66 +1,53 @@
package gui
import (
"os/exec"
"strconv"
"strings"
"github.com/jesseduffield/gocui"
)
// runSyncOrAsyncCommand takes the output of a command that may have returned
// either no error, an error, or a subprocess to execute, and if a subprocess
// needs to be set on the gui object, it does so, and then returns the error
// the bool returned tells us whether the calling code should continue
func (gui *Gui) runSyncOrAsyncCommand(sub *exec.Cmd, err error) (bool, error) {
if err != nil {
if err != gui.Errors.ErrSubProcess {
return false, gui.createErrorPanel(gui.g, err.Error())
}
}
if sub != nil {
gui.SubProcess = sub
return false, gui.Errors.ErrSubProcess
}
return true, nil
}
func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
message := gui.trimmedContent(v)
if message == "" {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CommitWithoutMessageErr"))
}
flags := ""
skipHookPrefix := gui.Config.GetUserConfig().GetString("git.skipHookPrefix")
if skipHookPrefix != "" && strings.HasPrefix(message, skipHookPrefix) {
flags = "--no-verify"
}
ok, err := gui.runSyncOrAsyncCommand(gui.GitCommand.Commit(message, flags))
sub, err := gui.GitCommand.Commit(g, message)
if err != nil {
return err
// TODO need to find a way to send through this error
if err != gui.Errors.ErrSubProcess {
return gui.createErrorPanel(g, err.Error())
}
}
if !ok {
return nil
if sub != nil {
gui.SubProcess = sub
return gui.Errors.ErrSubProcess
}
gui.refreshFiles(g)
v.Clear()
_ = v.SetCursor(0, 0)
_ = v.SetOrigin(0, 0)
_, _ = g.SetViewOnBottom("commitMessage")
_ = gui.switchFocus(g, v, gui.getFilesView())
return gui.refreshSidePanels(g)
v.SetCursor(0, 0)
g.SetViewOnBottom("commitMessage")
gui.switchFocus(g, v, gui.getFilesView(g))
return gui.refreshCommits(g)
}
func (gui *Gui) handleCommitClose(g *gocui.Gui, v *gocui.View) error {
g.SetViewOnBottom("commitMessage")
return gui.switchFocus(g, v, gui.getFilesView())
return gui.switchFocus(g, v, gui.getFilesView(g))
}
func (gui *Gui) handleNewlineCommitMessage(g *gocui.Gui, v *gocui.View) error {
// resising ahead of time so that the top line doesn't get hidden to make
// room for the cursor on the second line
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, v.Buffer())
if _, err := g.SetView("commitMessage", x0, y0, x1, y1+1, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
}
v.EditNewLine()
return nil
}
func (gui *Gui) handleCommitFocused(g *gocui.Gui, v *gocui.View) error {
if _, err := g.SetViewOnTop("commitMessage"); err != nil {
return err
}
message := gui.Tr.TemplateLocalize(
"CloseConfirm",
Teml{
@@ -70,16 +57,3 @@ func (gui *Gui) handleCommitFocused(g *gocui.Gui, v *gocui.View) error {
)
return gui.renderString(g, "options", message)
}
func (gui *Gui) getBufferLength(view *gocui.View) string {
return " " + strconv.Itoa(strings.Count(view.Buffer(), "")-1) + " "
}
// RenderCommitLength is a function.
func (gui *Gui) RenderCommitLength() {
if !gui.Config.GetUserConfig().GetBool("gui.commitLength.show") {
return
}
v := gui.getCommitMessageView()
v.Subtitle = gui.getBufferLength(v)
}
+90 -523
View File
@@ -1,170 +1,111 @@
package gui
import (
"fmt"
"strconv"
"errors"
"github.com/fatih/color"
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/git"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// list panel functions
func (gui *Gui) getSelectedCommit(g *gocui.Gui) *commands.Commit {
selectedLine := gui.State.Panels.Commits.SelectedLine
if selectedLine == -1 {
return nil
}
return gui.State.Commits[selectedLine]
}
func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
commit := gui.getSelectedCommit(g)
if commit == nil {
return gui.renderString(g, "main", gui.Tr.SLocalize("NoCommitsThisBranch"))
}
if err := gui.focusPoint(0, gui.State.Panels.Commits.SelectedLine, len(gui.State.Commits), v); err != nil {
return err
}
// if specific diff mode is on, don't show diff
if gui.State.Panels.Commits.SpecificDiffMode {
return nil
}
commitText, err := gui.GitCommand.Show(commit.Sha)
if err != nil {
return err
}
return gui.renderString(g, "main", commitText)
}
func (gui *Gui) refreshCommits(g *gocui.Gui) error {
g.Update(func(*gocui.Gui) error {
builder, err := git.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr, gui.State.CherryPickedCommits, gui.State.DiffEntries)
gui.State.Commits = gui.GitCommand.GetCommits()
v, err := g.View("commits")
if err != nil {
return err
panic(err)
}
commits, err := builder.GetCommits()
if err != nil {
return err
}
gui.State.Commits = commits
gui.refreshSelectedLine(&gui.State.Panels.Commits.SelectedLine, len(gui.State.Commits))
isFocused := gui.g.CurrentView().Name() == "commits"
list, err := utils.RenderList(gui.State.Commits, isFocused)
if err != nil {
return err
}
v := gui.getCommitsView()
v.Clear()
fmt.Fprint(v, list)
gui.refreshStatus(g)
if g.CurrentView() == v {
gui.handleCommitSelect(g, v)
red := color.New(color.FgRed)
yellow := color.New(color.FgYellow)
white := color.New(color.FgWhite)
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)
}
if g.CurrentView() == gui.getCommitFilesView() {
return gui.refreshCommitFilesView()
gui.refreshStatus(g)
if g.CurrentView().Name() == "commits" {
gui.handleCommitSelect(g, v)
}
return nil
})
return nil
}
func (gui *Gui) handleCommitsNextLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
panelState := gui.State.Panels.Commits
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Commits), false)
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
return gui.handleCommitSelect(gui.g, v)
}
func (gui *Gui) handleCommitsPrevLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
panelState := gui.State.Panels.Commits
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Commits), true)
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
return gui.handleCommitSelect(gui.g, v)
}
// specific functions
func (gui *Gui) handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error {
return gui.createConfirmationPanel(g, commitView, gui.Tr.SLocalize("ResetToCommit"), gui.Tr.SLocalize("SureResetThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedCommit(g)
if commit == nil {
panic(errors.New(gui.Tr.SLocalize("NoCommitsThisBranch")))
commit, err := gui.getSelectedCommit(g)
if err != nil {
panic(err)
}
if err := gui.GitCommand.ResetToCommit(commit.Sha, "mixed"); err != nil {
if err := gui.GitCommand.ResetToCommit(commit.Sha); err != nil {
return gui.createErrorPanel(g, err.Error())
}
if err := gui.refreshCommits(g); err != nil {
panic(err)
}
if err := gui.refreshFiles(); err != nil {
if err := gui.refreshFiles(g); err != nil {
panic(err)
}
gui.resetOrigin(commitView)
gui.State.Panels.Commits.SelectedLine = 0
return gui.handleCommitSelect(g, commitView)
return gui.handleCommitSelect(g, nil)
}, nil)
}
func (gui *Gui) renderCommitsOptions(g *gocui.Gui) error {
return gui.renderOptionsMap(g, map[string]string{
"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 {
if err := gui.renderCommitsOptions(g); err != nil {
return err
}
commit, err := gui.getSelectedCommit(g)
if err != nil {
if err != errors.New(gui.Tr.SLocalize("NoCommitsThisBranch")) {
return err
}
return gui.renderString(g, "main", gui.Tr.SLocalize("NoCommitsThisBranch"))
}
commitText := gui.GitCommand.Show(commit.Sha)
return gui.renderString(g, "main", commitText)
}
func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
if len(gui.State.Commits) <= 1 {
if gui.getItemPosition(v) != 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlySquashTopmostCommit"))
}
if len(gui.State.Commits) == 1 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash"))
}
applied, err := gui.handleMidRebaseCommand("squash")
commit, err := gui.getSelectedCommit(g)
if err != nil {
return err
}
if applied {
return nil
if err := gui.GitCommand.SquashPreviousTwoCommits(commit.Name); err != nil {
return gui.createErrorPanel(g, err.Error())
}
gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("Squash"), gui.Tr.SLocalize("SureSquashThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("SquashingStatus"), func() error {
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "squash")
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
return nil
if err := gui.refreshCommits(g); err != nil {
panic(err)
}
gui.refreshStatus(g)
return gui.handleCommitSelect(g, v)
}
// 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 {
if file.Tracked && file.HasUnstagedChanges {
return true
@@ -174,40 +115,35 @@ func (gui *Gui) anyUnStagedChanges(files []*commands.File) bool {
}
func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
if len(gui.State.Commits) <= 1 {
if len(gui.State.Commits) == 1 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash"))
}
applied, err := gui.handleMidRebaseCommand("fixup")
if gui.anyUnStagedChanges(gui.State.Files) {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantFixupWhileUnstagedChanges"))
}
branch := gui.State.Branches[0]
commit, err := gui.getSelectedCommit(g)
if err != nil {
return err
}
if applied {
return nil
}
gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("Fixup"), gui.Tr.SLocalize("SureFixupThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("FixingStatus"), func() error {
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "fixup")
return gui.handleGenericMergeCommandResult(err)
})
message := gui.Tr.SLocalize("SureFixupThisCommit")
gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("Fixup"), message, func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.SquashFixupCommit(branch.Name, commit.Sha); err != nil {
return gui.createErrorPanel(g, err.Error())
}
if err := gui.refreshCommits(g); err != nil {
panic(err)
}
return gui.refreshStatus(g)
}, nil)
return nil
}
func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
applied, err := gui.handleMidRebaseCommand("reword")
if err != nil {
return err
}
if applied {
return nil
}
if gui.State.Panels.Commits.SelectedLine != 0 {
if gui.getItemPosition(v) != 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlyRenameTopCommit"))
}
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("renameCommit"), func(g *gocui.Gui, v *gocui.View) error {
gui.createPromptPanel(g, v, gui.Tr.SLocalize("RenameCommit"), func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.RenameCommit(v.Buffer()); err != nil {
return gui.createErrorPanel(g, err.Error())
}
@@ -216,390 +152,21 @@ func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
}
return gui.handleCommitSelect(g, v)
})
}
func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
applied, err := gui.handleMidRebaseCommand("reword")
if err != nil {
return err
}
if applied {
return nil
}
subProcess, err := gui.GitCommand.RewordCommit(gui.State.Commits, gui.State.Panels.Commits.SelectedLine)
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
if subProcess != nil {
gui.SubProcess = subProcess
return gui.Errors.ErrSubProcess
}
return nil
}
// handleMidRebaseCommand sees if the selected commit is in fact a rebasing
// commit meaning you are trying to edit the todo file rather than actually
// begin a rebase. It then updates the todo file with that action
func (gui *Gui) handleMidRebaseCommand(action string) (bool, error) {
selectedCommit := gui.State.Commits[gui.State.Panels.Commits.SelectedLine]
if selectedCommit.Status != "rebasing" {
return false, nil
}
// for now we do not support setting 'reword' because it requires an editor
// and that means we either unconditionally wait around for the subprocess to ask for
// our input or we set a lazygit client as the EDITOR env variable and have it
// request us to edit the commit message when prompted.
if action == "reword" {
return true, gui.createErrorPanel(gui.g, gui.Tr.SLocalize("rewordNotSupported"))
}
if err := gui.GitCommand.EditRebaseTodo(gui.State.Panels.Commits.SelectedLine, action); err != nil {
return false, gui.createErrorPanel(gui.g, err.Error())
}
return true, gui.refreshCommits(gui.g)
}
// handleMoveTodoDown like handleMidRebaseCommand but for moving an item up in the todo list
func (gui *Gui) handleMoveTodoDown(index int) (bool, error) {
selectedCommit := gui.State.Commits[index]
if selectedCommit.Status != "rebasing" {
return false, nil
}
if gui.State.Commits[index+1].Status != "rebasing" {
return true, nil
}
if err := gui.GitCommand.MoveTodoDown(index); err != nil {
return true, gui.createErrorPanel(gui.g, err.Error())
}
return true, gui.refreshCommits(gui.g)
}
func (gui *Gui) handleCommitDelete(g *gocui.Gui, v *gocui.View) error {
applied, err := gui.handleMidRebaseCommand("drop")
func (gui *Gui) getSelectedCommit(g *gocui.Gui) (commands.Commit, error) {
v, err := g.View("commits")
if err != nil {
return err
panic(err)
}
if applied {
return nil
if len(gui.State.Commits) == 0 {
return commands.Commit{}, errors.New(gui.Tr.SLocalize("NoCommitsThisBranch"))
}
return gui.createConfirmationPanel(gui.g, v, gui.Tr.SLocalize("DeleteCommitTitle"), gui.Tr.SLocalize("DeleteCommitPrompt"), func(*gocui.Gui, *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("DeletingStatus"), func() error {
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "drop")
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
}
func (gui *Gui) handleCommitMoveDown(g *gocui.Gui, v *gocui.View) error {
index := gui.State.Panels.Commits.SelectedLine
selectedCommit := gui.State.Commits[index]
if selectedCommit.Status == "rebasing" {
if gui.State.Commits[index+1].Status != "rebasing" {
return nil
}
if err := gui.GitCommand.MoveTodoDown(index); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
gui.State.Panels.Commits.SelectedLine++
return gui.refreshCommits(gui.g)
lineNumber := gui.getItemPosition(v)
if lineNumber > len(gui.State.Commits)-1 {
gui.Log.Info(gui.Tr.SLocalize("PotentialErrInGetselectedCommit"), gui.State.Commits, lineNumber)
return gui.State.Commits[len(gui.State.Commits)-1], nil
}
return gui.WithWaitingStatus(gui.Tr.SLocalize("MovingStatus"), func() error {
err := gui.GitCommand.MoveCommitDown(gui.State.Commits, index)
if err == nil {
gui.State.Panels.Commits.SelectedLine++
}
return gui.handleGenericMergeCommandResult(err)
})
}
func (gui *Gui) handleCommitMoveUp(g *gocui.Gui, v *gocui.View) error {
index := gui.State.Panels.Commits.SelectedLine
if index == 0 {
return nil
}
selectedCommit := gui.State.Commits[index]
if selectedCommit.Status == "rebasing" {
if err := gui.GitCommand.MoveTodoDown(index - 1); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
gui.State.Panels.Commits.SelectedLine--
return gui.refreshCommits(gui.g)
}
return gui.WithWaitingStatus(gui.Tr.SLocalize("MovingStatus"), func() error {
err := gui.GitCommand.MoveCommitDown(gui.State.Commits, index-1)
if err == nil {
gui.State.Panels.Commits.SelectedLine--
}
return gui.handleGenericMergeCommandResult(err)
})
}
func (gui *Gui) handleCommitEdit(g *gocui.Gui, v *gocui.View) error {
applied, err := gui.handleMidRebaseCommand("edit")
if err != nil {
return err
}
if applied {
return nil
}
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
err = gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "edit")
return gui.handleGenericMergeCommandResult(err)
})
}
func (gui *Gui) handleCommitAmendTo(g *gocui.Gui, v *gocui.View) error {
return gui.createConfirmationPanel(gui.g, v, gui.Tr.SLocalize("AmendCommitTitle"), gui.Tr.SLocalize("AmendCommitPrompt"), func(*gocui.Gui, *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("AmendingStatus"), func() error {
err := gui.GitCommand.AmendTo(gui.State.Commits[gui.State.Panels.Commits.SelectedLine].Sha)
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
}
func (gui *Gui) handleCommitPick(g *gocui.Gui, v *gocui.View) error {
applied, err := gui.handleMidRebaseCommand("pick")
if err != nil {
return err
}
if applied {
return nil
}
// at this point we aren't actually rebasing so we will interpret this as an
// attempt to pull. We might revoke this later after enabling configurable keybindings
return gui.pullFiles(g, v)
}
func (gui *Gui) handleCommitRevert(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.Revert(gui.State.Commits[gui.State.Panels.Commits.SelectedLine].Sha); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
gui.State.Panels.Commits.SelectedLine++
return gui.refreshCommits(gui.g)
}
func (gui *Gui) handleCopyCommit(g *gocui.Gui, v *gocui.View) error {
// get currently selected commit, add the sha to state.
commit := gui.State.Commits[gui.State.Panels.Commits.SelectedLine]
// we will un-copy it if it's already copied
for index, cherryPickedCommit := range gui.State.CherryPickedCommits {
if commit.Sha == cherryPickedCommit.Sha {
gui.State.CherryPickedCommits = append(gui.State.CherryPickedCommits[0:index], gui.State.CherryPickedCommits[index+1:]...)
return gui.refreshCommits(gui.g)
}
}
gui.addCommitToCherryPickedCommits(gui.State.Panels.Commits.SelectedLine)
return gui.refreshCommits(gui.g)
}
func (gui *Gui) addCommitToCherryPickedCommits(index int) {
// not super happy with modifying the state of the Commits array here
// but the alternative would be very tricky
gui.State.Commits[index].Copied = true
newCommits := []*commands.Commit{}
for _, commit := range gui.State.Commits {
if commit.Copied {
// duplicating just the things we need to put in the rebase TODO list
newCommits = append(newCommits, &commands.Commit{Name: commit.Name, Sha: commit.Sha})
}
}
gui.State.CherryPickedCommits = newCommits
}
func (gui *Gui) handleCopyCommitRange(g *gocui.Gui, v *gocui.View) error {
// whenever I add a commit, I need to make sure I retain its order
// find the last commit that is copied that's above our position
// if there are none, startIndex = 0
startIndex := 0
for index, commit := range gui.State.Commits[0:gui.State.Panels.Commits.SelectedLine] {
if commit.Copied {
startIndex = index
}
}
gui.Log.Info("commit copy start index: " + strconv.Itoa(startIndex))
for index := startIndex; index <= gui.State.Panels.Commits.SelectedLine; index++ {
gui.addCommitToCherryPickedCommits(index)
}
return gui.refreshCommits(gui.g)
}
// HandlePasteCommits begins a cherry-pick rebase with the commits the user has copied
func (gui *Gui) HandlePasteCommits(g *gocui.Gui, v *gocui.View) error {
return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("CherryPick"), gui.Tr.SLocalize("SureCherryPick"), func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("CherryPickingStatus"), func() error {
err := gui.GitCommand.CherryPickCommits(gui.State.CherryPickedCommits)
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
}
func (gui *Gui) handleSwitchToCommitFilesPanel(g *gocui.Gui, v *gocui.View) error {
if err := gui.refreshCommitFilesView(); err != nil {
return err
}
return gui.switchFocus(g, v, gui.getCommitFilesView())
}
func (gui *Gui) handleToggleDiffCommit(g *gocui.Gui, v *gocui.View) error {
selectLimit := 2
// get selected commit
commit := gui.getSelectedCommit(g)
if commit == nil {
return gui.renderString(g, "main", gui.Tr.SLocalize("NoCommitsThisBranch"))
}
// if already selected commit delete
if idx, has := gui.hasCommit(gui.State.DiffEntries, commit.Sha); has {
gui.State.DiffEntries = gui.unchooseCommit(gui.State.DiffEntries, idx)
} else {
if len(gui.State.DiffEntries) == selectLimit {
gui.State.DiffEntries = gui.unchooseCommit(gui.State.DiffEntries, 0)
}
gui.State.DiffEntries = append(gui.State.DiffEntries, commit)
}
gui.setDiffMode()
// if selected two commits, display diff between
if len(gui.State.DiffEntries) == selectLimit {
commitText, err := gui.GitCommand.DiffCommits(gui.State.DiffEntries[0].Sha, gui.State.DiffEntries[1].Sha)
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.renderString(g, "main", commitText)
}
return nil
}
func (gui *Gui) setDiffMode() {
v := gui.getCommitsView()
if len(gui.State.DiffEntries) != 0 {
gui.State.Panels.Commits.SpecificDiffMode = true
v.Title = gui.Tr.SLocalize("CommitsDiffTitle")
} else {
gui.State.Panels.Commits.SpecificDiffMode = false
v.Title = gui.Tr.SLocalize("CommitsTitle")
}
gui.refreshCommits(gui.g)
}
func (gui *Gui) hasCommit(commits []*commands.Commit, target string) (int, bool) {
for idx, commit := range commits {
if commit.Sha == target {
return idx, true
}
}
return -1, false
}
func (gui *Gui) unchooseCommit(commits []*commands.Commit, i int) []*commands.Commit {
return append(commits[:i], commits[i+1:]...)
}
func (gui *Gui) handleCreateFixupCommit(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedCommit(g)
if commit == nil {
return nil
}
return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("CreateFixupCommit"), gui.Tr.TemplateLocalize(
"SureCreateFixupCommit",
Teml{
"commit": commit.Sha,
},
), func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.CreateFixupCommit(commit.Sha); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return gui.refreshSidePanels(gui.g)
}, nil)
}
func (gui *Gui) handleSquashAllAboveFixupCommits(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedCommit(g)
if commit == nil {
return nil
}
return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("SquashAboveCommits"), gui.Tr.TemplateLocalize(
"SureSquashAboveCommits",
Teml{
"commit": commit.Sha,
},
), func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("SquashingStatus"), func() error {
err := gui.GitCommand.SquashAllAboveFixupCommits(commit.Sha)
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
}
type resetOption struct {
description string
command string
}
// GetDisplayStrings is a function.
func (r *resetOption) GetDisplayStrings(isFocused bool) []string {
return []string{r.description, color.New(color.FgRed).Sprint(r.command)}
}
func (gui *Gui) handleCreateCommitResetMenu(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedCommit(g)
if commit == nil {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NoCommitsThisBranch"))
}
strengths := []string{"soft", "mixed", "hard"}
options := make([]*resetOption, len(strengths))
for i, strength := range strengths {
options[i] = &resetOption{
description: fmt.Sprintf("%s reset", strength),
command: fmt.Sprintf("reset --%s %s", strength, commit.Sha),
}
}
handleMenuPress := func(index int) error {
if err := gui.GitCommand.ResetToCommit(commit.Sha, strengths[index]); err != nil {
return err
}
if err := gui.refreshCommits(g); err != nil {
return err
}
if err := gui.refreshFiles(); err != nil {
return err
}
if err := gui.resetOrigin(gui.getCommitsView()); err != nil {
return err
}
gui.State.Panels.Commits.SelectedLine = 0
return gui.handleCommitSelect(g, gui.getCommitsView())
}
return gui.createMenu(fmt.Sprintf("%s %s", gui.Tr.SLocalize("resetTo"), commit.Sha), options, len(options), handleMenuPress)
return gui.State.Commits[lineNumber], nil
}
+67 -83
View File
@@ -8,18 +8,17 @@ package gui
import (
"strings"
"time"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.View) error) func(*gocui.Gui, *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
if function != nil {
if err := function(g, v); err != nil {
return err
panic(err)
}
}
return gui.closeConfirmationPrompt(g)
@@ -29,7 +28,7 @@ func (gui *Gui) wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.Vie
func (gui *Gui) closeConfirmationPrompt(g *gocui.Gui) error {
view, err := g.View("confirmation")
if err != nil {
return nil // if it's already been closed we can just return
panic(err)
}
if err := gui.returnFocus(g, view); err != nil {
panic(err)
@@ -38,24 +37,19 @@ func (gui *Gui) closeConfirmationPrompt(g *gocui.Gui) error {
return g.DeleteView("confirmation")
}
func (gui *Gui) getMessageHeight(wrap bool, message string, width int) int {
func (gui *Gui) getMessageHeight(message string, width int) int {
lines := strings.Split(message, "\n")
lineCount := 0
// if we need to wrap, calculate height to fit content within view's width
if wrap {
for _, line := range lines {
lineCount += len(line)/width + 1
}
} else {
lineCount = len(lines)
for _, line := range lines {
lineCount += len(line)/width + 1
}
return lineCount
}
func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, wrap bool, prompt string) (int, int, int, int) {
func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, prompt string) (int, int, int, int) {
width, height := g.Size()
panelWidth := width / 2
panelHeight := gui.getMessageHeight(wrap, prompt, panelWidth)
panelHeight := gui.getMessageHeight(prompt, panelWidth)
return width/2 - panelWidth/2,
height/2 - panelHeight/2 - panelHeight%2 - 1,
width/2 + panelWidth/2,
@@ -64,51 +58,27 @@ func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, wrap bool, prompt s
func (gui *Gui) createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, handleConfirm func(*gocui.Gui, *gocui.View) error) error {
gui.onNewPopupPanel()
confirmationView, err := gui.prepareConfirmationPanel(currentView, title, "", false)
if err != nil {
return err
}
confirmationView.Editable = true
return gui.setKeyBindings(g, handleConfirm, nil)
}
func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt string, hasLoader bool) (*gocui.View, error) {
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, true, prompt)
confirmationView, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0)
if err != nil {
if err.Error() != "unknown view" {
return nil, err
// only need to fit one line
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, "")
if confirmationView, err := g.SetView("confirmation", x0, y0, x1, y1, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
confirmationView.HasLoader = hasLoader
confirmationView.Editable = true
confirmationView.Title = title
confirmationView.Wrap = true
confirmationView.FgColor = theme.GocuiDefaultTextColor
confirmationView.FgColor = gocui.ColorWhite
gui.switchFocus(g, currentView, confirmationView)
return gui.setKeyBindings(g, handleConfirm, nil)
}
gui.g.Update(func(g *gocui.Gui) error {
return gui.switchFocus(gui.g, currentView, confirmationView)
})
return confirmationView, nil
return nil
}
func (gui *Gui) onNewPopupPanel() {
viewNames := []string{"commitMessage",
"credentials",
"menu"}
for _, viewName := range viewNames {
_, _ = gui.g.SetViewOnBottom(viewName)
}
gui.g.SetViewOnBottom("commitMessage")
}
func (gui *Gui) createLoaderPanel(g *gocui.Gui, currentView *gocui.View, prompt string) error {
return gui.createPopupPanel(g, currentView, "", prompt, true, nil, nil)
}
// it is very important that within this function we never include the original prompt in any error messages, because it may contain e.g. a user password
func (gui *Gui) createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
return gui.createPopupPanel(g, currentView, title, prompt, false, handleConfirm, handleClose)
}
func (gui *Gui) createPopupPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, hasLoader bool, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
gui.onNewPopupPanel()
g.Update(func(g *gocui.Gui) error {
// delete the existing confirmation panel if it exists
@@ -123,19 +93,36 @@ func (gui *Gui) createPopupPanel(g *gocui.Gui, currentView *gocui.View, title, p
gui.Log.Error(errMessage)
}
}
confirmationView, err := gui.prepareConfirmationPanel(currentView, title, prompt, hasLoader)
if err != nil {
return err
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, prompt)
if confirmationView, err := g.SetView("confirmation", x0, y0, x1, y1, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
confirmationView.Title = title
confirmationView.FgColor = gocui.ColorWhite
gui.renderString(g, "confirmation", prompt)
gui.switchFocus(g, currentView, confirmationView)
return gui.setKeyBindings(g, handleConfirm, handleClose)
}
confirmationView.Editable = false
if err := gui.renderString(g, "confirmation", prompt); err != nil {
return err
}
return gui.setKeyBindings(g, handleConfirm, handleClose)
return nil
})
return nil
}
func (gui *Gui) handleNewline(g *gocui.Gui, v *gocui.View) error {
// resising ahead of time so that the top line doesn't get hidden to make
// room for the cursor on the second line
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, v.Buffer())
if _, err := g.SetView("confirmation", x0, y0, x1, y1+1, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
}
v.EditNewLine()
return nil
}
func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
actions := gui.Tr.TemplateLocalize(
"CloseConfirm",
@@ -144,40 +131,37 @@ func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*go
"keyBindConfirm": "enter",
},
)
if err := gui.renderString(g, "options", actions); err != nil {
gui.renderString(g, "options", actions)
if err := g.SetKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone, gui.wrappedConfirmationFunction(handleConfirm)); err != nil {
return err
}
if err := g.SetKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone, gui.wrappedConfirmationFunction(handleConfirm)); err != nil {
if err := g.SetKeybinding("confirmation", gocui.KeyTab, gocui.ModNone, gui.handleNewline); err != nil {
return err
}
return g.SetKeybinding("confirmation", gocui.KeyEsc, gocui.ModNone, gui.wrappedConfirmationFunction(handleClose))
}
func (gui *Gui) createMessagePanel(g *gocui.Gui, currentView *gocui.View, title, prompt string) error {
return gui.createPopupPanel(g, currentView, title, prompt, false, nil, nil)
}
// createSpecificErrorPanel allows you to create an error popup, specifying the
// view to be focused when the user closes the popup, and a boolean specifying
// whether we will log the error. If the message may include a user password,
// this function is to be used over the more generic createErrorPanel, with
// willLog set to false
func (gui *Gui) createSpecificErrorPanel(message string, nextView *gocui.View, willLog bool) error {
if willLog {
go func() {
// when reporting is switched on this log call sometimes introduces
// a delay on the error panel popping up. Here I'm adding a second wait
// so that the error is logged while the user is reading the error message
time.Sleep(time.Second)
gui.Log.Error(message)
}()
}
colorFunction := color.New(color.FgRed).SprintFunc()
coloredMessage := colorFunction(strings.TrimSpace(message))
return gui.createConfirmationPanel(gui.g, nextView, gui.Tr.SLocalize("Error"), coloredMessage, nil, nil)
return gui.createConfirmationPanel(g, currentView, title, prompt, nil, nil)
}
func (gui *Gui) createErrorPanel(g *gocui.Gui, message string) error {
return gui.createSpecificErrorPanel(message, g.CurrentView(), true)
currentView := g.CurrentView()
colorFunction := color.New(color.FgRed).SprintFunc()
coloredMessage := colorFunction(strings.TrimSpace(message))
return gui.createConfirmationPanel(g, currentView, gui.Tr.SLocalize("Error"), coloredMessage, nil, nil)
}
func (gui *Gui) resizePopupPanel(g *gocui.Gui, v *gocui.View) error {
// If the confirmation panel is already displayed, just resize the width,
// otherwise continue
content := utils.TrimTrailingNewline(v.Buffer())
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, content)
vx0, vy0, vx1, vy1 := v.Dimensions()
if vx0 == x0 && vy0 == y0 && vx1 == x1 && vy1 == y1 {
return nil
}
gui.Log.Info(gui.Tr.SLocalize("resizingPopupPanel"))
_, err := g.SetView(v.Name(), x0, y0, x1, y1, 0)
return err
}
-79
View File
@@ -1,79 +0,0 @@
package gui
func (gui *Gui) titleMap() map[string]string {
return map[string]string{
"commits": gui.Tr.SLocalize("DiffTitle"),
"branches": gui.Tr.SLocalize("LogTitle"),
"files": gui.Tr.SLocalize("DiffTitle"),
"status": "",
"stash": gui.Tr.SLocalize("DiffTitle"),
}
}
func (gui *Gui) contextTitleMap() map[string]map[string]string {
return map[string]map[string]string{
"main": {
"staging": gui.Tr.SLocalize("StagingMainTitle"),
"merging": gui.Tr.SLocalize("MergingMainTitle"),
"normal": "",
},
}
}
func (gui *Gui) setMainTitle() error {
currentView := gui.g.CurrentView()
if currentView == nil {
return nil
}
currentViewName := currentView.Name()
var newTitle string
if context, ok := gui.State.Contexts[currentViewName]; ok {
newTitle = gui.contextTitleMap()[currentViewName][context]
} else if title, ok := gui.titleMap()[currentViewName]; ok {
newTitle = title
} else {
return nil
}
gui.getMainView().Title = newTitle
return nil
}
func (gui *Gui) changeContext(viewName, context string) error {
if gui.State.Contexts[viewName] == context {
return nil
}
contextMap := gui.GetContextMap()
gui.g.DeleteKeybindings(viewName)
bindings := contextMap[viewName][context]
for _, binding := range bindings {
if err := gui.g.SetKeybinding(viewName, binding.Key, binding.Modifier, binding.Handler); err != nil {
return err
}
}
gui.State.Contexts[viewName] = context
return gui.setMainTitle()
}
func (gui *Gui) setInitialContexts() error {
contextMap := gui.GetContextMap()
initialContexts := map[string]string{
"main": "normal",
}
for viewName, context := range initialContexts {
bindings := contextMap[viewName][context]
for _, binding := range bindings {
if err := gui.g.SetKeybinding(binding.ViewName, binding.Key, binding.Modifier, binding.Handler); err != nil {
return err
}
}
}
gui.State.Contexts = initialContexts
return nil
}
-104
View File
@@ -1,104 +0,0 @@
package gui
import (
"strings"
"github.com/jesseduffield/gocui"
)
type credentials chan string
// waitForPassUname wait for a username or password input from the credentials popup
func (gui *Gui) waitForPassUname(g *gocui.Gui, currentView *gocui.View, passOrUname string) string {
gui.credentials = make(chan string)
g.Update(func(g *gocui.Gui) error {
credentialsView, _ := g.View("credentials")
if passOrUname == "username" {
credentialsView.Title = gui.Tr.SLocalize("CredentialsUsername")
credentialsView.Mask = 0
} else {
credentialsView.Title = gui.Tr.SLocalize("CredentialsPassword")
credentialsView.Mask = '*'
}
err := gui.switchFocus(g, currentView, credentialsView)
if err != nil {
return err
}
gui.RenderCommitLength()
return nil
})
// wait for username/passwords input
userInput := <-gui.credentials
return userInput + "\n"
}
func (gui *Gui) handleSubmitCredential(g *gocui.Gui, v *gocui.View) error {
message := gui.trimmedContent(v)
gui.credentials <- message
err := gui.refreshFiles()
if err != nil {
return err
}
v.Clear()
err = v.SetCursor(0, 0)
if err != nil {
return err
}
_, err = g.SetViewOnBottom("credentials")
if err != nil {
return err
}
nextView, err := gui.g.View("confirmation")
if err != nil {
nextView = gui.getFilesView()
}
err = gui.switchFocus(g, nil, nextView)
if err != nil {
return err
}
return gui.refreshCommits(g)
}
func (gui *Gui) handleCloseCredentialsView(g *gocui.Gui, v *gocui.View) error {
_, err := g.SetViewOnBottom("credentials")
if err != nil {
return err
}
gui.credentials <- ""
return gui.switchFocus(g, nil, gui.getFilesView())
}
func (gui *Gui) handleCredentialsViewFocused(g *gocui.Gui, v *gocui.View) error {
if _, err := g.SetViewOnTop("credentials"); err != nil {
return err
}
message := gui.Tr.TemplateLocalize(
"CloseConfirm",
Teml{
"keyBindClose": "esc",
"keyBindConfirm": "enter",
},
)
return gui.renderString(g, "options", message)
}
// HandleCredentialsPopup handles the views after executing a command that might ask for credentials
func (gui *Gui) HandleCredentialsPopup(g *gocui.Gui, popupOpened bool, cmdErr error) {
if popupOpened {
_, _ = gui.g.SetViewOnBottom("credentials")
}
if cmdErr != nil {
errMessage := cmdErr.Error()
if strings.Contains(errMessage, "Invalid username or password") {
errMessage = gui.Tr.SLocalize("PassUnameWrong")
}
// we are not logging this error because it may contain a password
_ = gui.createSpecificErrorPanel(errMessage, gui.getFilesView(), false)
} else {
_ = gui.closeConfirmationPrompt(g)
_ = gui.refreshSidePanels(g)
}
}
+219 -480
View File
@@ -7,141 +7,17 @@ import (
// "strings"
"fmt"
"os/exec"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// list panel functions
func (gui *Gui) getSelectedFile(g *gocui.Gui) (*commands.File, error) {
selectedLine := gui.State.Panels.Files.SelectedLine
if selectedLine == -1 {
return &commands.File{}, gui.Errors.ErrNoFiles
}
return gui.State.Files[selectedLine], nil
}
func (gui *Gui) handleFilesFocus(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
cx, cy := v.Cursor()
_, oy := v.Origin()
prevSelectedLine := gui.State.Panels.Files.SelectedLine
newSelectedLine := cy - oy
if newSelectedLine > len(gui.State.Files)-1 || len(utils.Decolorise(gui.State.Files[newSelectedLine].DisplayString)) < cx {
return gui.handleFileSelect(gui.g, v, false)
}
gui.State.Panels.Files.SelectedLine = newSelectedLine
if prevSelectedLine == newSelectedLine && gui.currentViewName() == v.Name() {
return gui.handleFilePress(gui.g, v)
} else {
return gui.handleFileSelect(gui.g, v, true)
}
}
func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View, alreadySelected bool) error {
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
file, err := gui.getSelectedFile(g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
}
return gui.renderString(g, "main", gui.Tr.SLocalize("NoChangedFiles"))
}
if err := gui.focusPoint(0, gui.State.Panels.Files.SelectedLine, len(gui.State.Files), v); err != nil {
return err
}
if file.HasInlineMergeConflicts {
return gui.refreshMergePanel()
}
content := gui.GitCommand.Diff(file, false)
if alreadySelected {
g.Update(func(*gocui.Gui) error {
return gui.setViewContent(gui.g, gui.getMainView(), content)
})
return nil
}
return gui.renderString(g, "main", content)
}
func (gui *Gui) refreshFiles() error {
selectedFile, _ := gui.getSelectedFile(gui.g)
filesView := gui.getFilesView()
if filesView == nil {
// if the filesView hasn't been instantiated yet we just return
return nil
}
if err := gui.refreshStateFiles(); err != nil {
return err
}
gui.g.Update(func(g *gocui.Gui) error {
filesView.Clear()
isFocused := gui.g.CurrentView().Name() == "files"
list, err := utils.RenderList(gui.State.Files, isFocused)
if err != nil {
return err
}
fmt.Fprint(filesView, list)
if filesView == g.CurrentView() {
newSelectedFile, _ := gui.getSelectedFile(gui.g)
alreadySelected := newSelectedFile.Name == selectedFile.Name
return gui.handleFileSelect(g, filesView, alreadySelected)
}
return nil
})
return nil
}
func (gui *Gui) handleFilesNextLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
panelState := gui.State.Panels.Files
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Files), false)
return gui.handleFileSelect(gui.g, v, false)
}
func (gui *Gui) handleFilesPrevLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
panelState := gui.State.Panels.Files
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.Files), true)
return gui.handleFileSelect(gui.g, v, false)
}
// specific functions
func (gui *Gui) stagedFiles() []*commands.File {
func (gui *Gui) stagedFiles() []commands.File {
files := gui.State.Files
result := make([]*commands.File, 0)
result := make([]commands.File, 0)
for _, file := range files {
if file.HasStagedChanges {
result = append(result, file)
@@ -150,9 +26,9 @@ func (gui *Gui) stagedFiles() []*commands.File {
return result
}
func (gui *Gui) trackedFiles() []*commands.File {
func (gui *Gui) trackedFiles() []commands.File {
files := gui.State.Files
result := make([]*commands.File, 0)
result := make([]commands.File, 0)
for _, file := range files {
if file.Tracked {
result = append(result, file)
@@ -169,29 +45,6 @@ func (gui *Gui) stageSelectedFile(g *gocui.Gui) error {
return gui.GitCommand.StageFile(file.Name)
}
func (gui *Gui) handleEnterFile(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
}
return nil
}
if file.HasInlineMergeConflicts {
return gui.handleSwitchToMerge(g, v)
}
if !file.HasUnstagedChanges || file.HasMergeConflicts {
return gui.createErrorPanel(g, gui.Tr.SLocalize("FileStagingRequirements"))
}
if err := gui.changeContext("main", "staging"); err != nil {
return err
}
if err := gui.switchFocus(g, v, gui.getMainView()); err != nil {
return err
}
return gui.refreshStagingPanel()
}
func (gui *Gui) handleFilePress(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
@@ -201,7 +54,7 @@ func (gui *Gui) handleFilePress(g *gocui.Gui, v *gocui.View) error {
return err
}
if file.HasInlineMergeConflicts {
if file.HasMergeConflicts {
return gui.handleSwitchToMerge(g, v)
}
@@ -211,38 +64,11 @@ func (gui *Gui) handleFilePress(g *gocui.Gui, v *gocui.View) error {
gui.GitCommand.UnStageFile(file.Name, file.Tracked)
}
if err := gui.refreshFiles(); err != nil {
if err := gui.refreshFiles(g); err != nil {
return err
}
return gui.handleFileSelect(g, v, true)
}
func (gui *Gui) allFilesStaged() bool {
for _, file := range gui.State.Files {
if file.HasUnstagedChanges {
return false
}
}
return true
}
func (gui *Gui) handleStageAll(g *gocui.Gui, v *gocui.View) error {
var err error
if gui.allFilesStaged() {
err = gui.GitCommand.UnstageAll()
} else {
err = gui.GitCommand.StageAll()
}
if err != nil {
_ = gui.createErrorPanel(g, err.Error())
}
if err := gui.refreshFiles(); err != nil {
return err
}
return gui.handleFileSelect(g, v, false)
return gui.handleFileSelect(g, v)
}
func (gui *Gui) handleAddPatch(g *gocui.Gui, v *gocui.View) error {
@@ -259,11 +85,55 @@ func (gui *Gui) handleAddPatch(g *gocui.Gui, v *gocui.View) error {
if !file.Tracked {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CannotGitAdd"))
}
gui.SubProcess = gui.GitCommand.AddPatch(file.Name)
sub, err := gui.GitCommand.AddPatch(file.Name)
if err != nil {
return err
}
gui.SubProcess = sub
return gui.Errors.ErrSubProcess
}
func (gui *Gui) getSelectedFile(g *gocui.Gui) (commands.File, error) {
if len(gui.State.Files) == 0 {
return commands.File{}, gui.Errors.ErrNoFiles
}
filesView, err := g.View("files")
if err != nil {
panic(err)
}
lineNumber := gui.getItemPosition(filesView)
return gui.State.Files[lineNumber], nil
}
func (gui *Gui) handleFileRemove(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
if err == gui.Errors.ErrNoFiles {
return nil
}
return err
}
var deleteVerb string
if file.Tracked {
deleteVerb = gui.Tr.SLocalize("checkout")
} else {
deleteVerb = gui.Tr.SLocalize("delete")
}
message := gui.Tr.TemplateLocalize(
"SureTo",
Teml{
"deleteVerb": deleteVerb,
"fileName": file.Name,
},
)
return gui.createConfirmationPanel(g, v, strings.Title(deleteVerb)+" file", message, func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.RemoveFile(file); err != nil {
panic(err)
}
return gui.refreshFiles(g)
}, nil)
}
func (gui *Gui) handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
@@ -272,70 +142,73 @@ func (gui *Gui) handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
if file.Tracked {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantIgnoreTrackFiles"))
}
if err := gui.GitCommand.Ignore(file.Name); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return gui.refreshFiles()
gui.GitCommand.Ignore(file.Name)
return gui.refreshFiles(g)
}
func (gui *Gui) handleWIPCommitPress(g *gocui.Gui, filesView *gocui.View) error {
skipHookPreifx := gui.Config.GetUserConfig().GetString("git.skipHookPrefix")
if skipHookPreifx == "" {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("SkipHookPrefixNotConfigured"))
func (gui *Gui) renderfilesOptions(g *gocui.Gui, file *commands.File) error {
optionsMap := map[string]string{
"← → ↑ ↓": 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"),
"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 {
file, err := gui.getSelectedFile(g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
}
gui.renderString(g, "main", gui.Tr.SLocalize("NoChangedFiles"))
return gui.renderfilesOptions(g, nil)
}
gui.renderfilesOptions(g, &file)
var content string
if file.HasMergeConflicts {
return gui.refreshMergePanel(g)
}
if err := gui.renderString(g, "commitMessage", skipHookPreifx); err != nil {
return err
}
if err := gui.getCommitMessageView().SetCursor(len(skipHookPreifx), 0); err != nil {
return err
}
return gui.handleCommitPress(g, filesView)
content = gui.GitCommand.Diff(file)
return gui.renderString(g, "main", content)
}
func (gui *Gui) handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
if len(gui.stagedFiles()) == 0 && gui.State.WorkingTreeState == "normal" {
if len(gui.stagedFiles()) == 0 && !gui.State.HasMergeConflicts {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit"))
}
commitMessageView := gui.getCommitMessageView()
commitMessageView := gui.getCommitMessageView(g)
g.Update(func(g *gocui.Gui) error {
g.SetViewOnTop("commitMessage")
gui.switchFocus(g, filesView, commitMessageView)
gui.RenderCommitLength()
return nil
})
return nil
}
func (gui *Gui) handleAmendCommitPress(g *gocui.Gui, filesView *gocui.View) error {
if len(gui.stagedFiles()) == 0 && gui.State.WorkingTreeState == "normal" {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit"))
}
if len(gui.State.Commits) == 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoCommitToAmend"))
}
title := strings.Title(gui.Tr.SLocalize("AmendLastCommit"))
question := gui.Tr.SLocalize("SureToAmend")
return gui.createConfirmationPanel(g, filesView, title, question, func(g *gocui.Gui, v *gocui.View) error {
ok, err := gui.runSyncOrAsyncCommand(gui.GitCommand.AmendHead())
if err != nil {
return err
}
if !ok {
return nil
}
return gui.refreshSidePanels(g)
}, nil)
}
// handleCommitEditorPress - handle when the user wants to commit changes via
// their editor rather than via the popup panel
func (gui *Gui) handleCommitEditorPress(g *gocui.Gui, filesView *gocui.View) error {
if len(gui.stagedFiles()) == 0 && gui.State.WorkingTreeState == "normal" {
if len(gui.stagedFiles()) == 0 && !gui.State.HasMergeConflicts {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit"))
}
gui.PrepareSubProcess(g, "git", "commit")
@@ -343,45 +216,99 @@ func (gui *Gui) handleCommitEditorPress(g *gocui.Gui, filesView *gocui.View) err
}
// PrepareSubProcess - prepare a subprocess for execution and tell the gui to switch to it
func (gui *Gui) PrepareSubProcess(g *gocui.Gui, commands ...string) {
gui.SubProcess = gui.GitCommand.PrepareCommitSubProcess()
func (gui *Gui) PrepareSubProcess(g *gocui.Gui, commands ...string) error {
sub, err := gui.GitCommand.PrepareCommitSubProcess()
if err != nil {
return err
}
gui.SubProcess = sub
g.Update(func(g *gocui.Gui) error {
return gui.Errors.ErrSubProcess
})
return nil
}
func (gui *Gui) editFile(filename string) error {
_, err := gui.runSyncOrAsyncCommand(gui.OSCommand.EditFile(filename))
return err
func (gui *Gui) genericFileOpen(g *gocui.Gui, v *gocui.View, filename string, open func(string) (*exec.Cmd, error)) error {
sub, err := open(filename)
if err != nil {
return gui.createErrorPanel(g, err.Error())
}
if sub != nil {
gui.SubProcess = sub
return gui.Errors.ErrSubProcess
}
return nil
}
func (gui *Gui) handleFileEdit(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return err
}
return gui.editFile(file.Name)
return gui.genericFileOpen(g, v, file.Name, gui.OSCommand.EditFile)
}
func (gui *Gui) handleFileOpen(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return err
}
return gui.openFile(file.Name)
return gui.genericFileOpen(g, v, file.Name, gui.OSCommand.OpenFile)
}
func (gui *Gui) handleSublimeFileOpen(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
return err
}
return gui.genericFileOpen(g, v, file.Name, gui.OSCommand.SublimeOpenFile)
}
func (gui *Gui) handleVsCodeFileOpen(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
return err
}
return gui.genericFileOpen(g, v, file.Name, gui.OSCommand.VsCodeOpenFile)
}
func (gui *Gui) handleRefreshFiles(g *gocui.Gui, v *gocui.View) error {
return gui.refreshFiles()
return gui.refreshFiles(g)
}
func (gui *Gui) refreshStateFiles() error {
func (gui *Gui) refreshStateFiles() {
// get files to stage
files := gui.GitCommand.GetStatusFiles()
gui.State.Files = gui.GitCommand.MergeStatusFiles(gui.State.Files, files)
gui.refreshSelectedLine(&gui.State.Panels.Files.SelectedLine, len(gui.State.Files))
return gui.updateWorkTreeState()
gui.updateHasMergeConflictStatus()
}
func (gui *Gui) updateHasMergeConflictStatus() error {
merging, err := gui.GitCommand.IsInMergeState()
if err != nil {
return err
}
gui.State.HasMergeConflicts = merging
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) {
@@ -392,79 +319,77 @@ func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) {
}
return "", gui.renderString(g, "main", gui.Tr.SLocalize("NoFilesDisplay"))
}
if item.Type != "file" {
return "", gui.renderString(g, "main", gui.Tr.SLocalize("NotAFile"))
}
cat, err := gui.GitCommand.CatFile(item.Name)
if err != nil {
gui.Log.Error(err)
return "", gui.renderString(g, "main", err.Error())
panic(err)
}
return cat, nil
}
func (gui *Gui) pullFiles(g *gocui.Gui, v *gocui.View) error {
if err := gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("PullWait")); err != nil {
func (gui *Gui) refreshFiles(g *gocui.Gui) error {
filesView, err := g.View("files")
if err != nil {
return err
}
go func() {
unamePassOpend := false
err := gui.GitCommand.Pull(func(passOrUname string) string {
unamePassOpend = true
return gui.waitForPassUname(g, v, passOrUname)
})
gui.HandleCredentialsPopup(g, unamePassOpend, err)
}()
gui.refreshStateFiles()
filesView.Clear()
for _, file := range gui.State.Files {
gui.renderFile(file, filesView)
}
gui.correctCursor(filesView)
if filesView == g.CurrentView() {
gui.handleFileSelect(g, filesView)
}
return nil
}
func (gui *Gui) pushWithForceFlag(g *gocui.Gui, v *gocui.View, force bool) error {
if err := gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("PushWait")); err != nil {
return err
}
func (gui *Gui) pullFiles(g *gocui.Gui, v *gocui.View) error {
gui.createMessagePanel(g, v, "", gui.Tr.SLocalize("PullWait"))
go func() {
unamePassOpend := false
branchName := gui.State.Branches[0].Name
err := gui.GitCommand.Push(branchName, force, func(passOrUname string) string {
unamePassOpend = true
return gui.waitForPassUname(g, v, passOrUname)
})
gui.HandleCredentialsPopup(g, unamePassOpend, err)
if err := gui.GitCommand.Pull(); err != nil {
gui.createErrorPanel(g, err.Error())
} else {
gui.closeConfirmationPrompt(g)
gui.refreshCommits(g)
gui.refreshStatus(g)
}
gui.refreshFiles(g)
}()
return nil
}
func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error {
// if we have pullables we'll ask if the user wants to force push
_, pullables := gui.GitCommand.GetCurrentBranchUpstreamDifferenceCount()
if pullables == "?" || pullables == "0" {
return gui.pushWithForceFlag(g, v, false)
}
err := gui.createConfirmationPanel(g, nil, gui.Tr.SLocalize("ForcePush"), gui.Tr.SLocalize("ForcePushPrompt"), func(g *gocui.Gui, v *gocui.View) error {
return gui.pushWithForceFlag(g, v, true)
}, nil)
return err
gui.createMessagePanel(g, v, "", gui.Tr.SLocalize("PushWait"))
go func() {
branchName := gui.State.Branches[0].Name
if err := gui.GitCommand.Push(branchName); err != nil {
gui.createErrorPanel(g, err.Error())
} else {
gui.closeConfirmationPrompt(g)
gui.refreshCommits(g)
gui.refreshStatus(g)
}
}()
return nil
}
func (gui *Gui) handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error {
mergeView, err := g.View("main")
if err != nil {
return err
}
file, err := gui.getSelectedFile(g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
return gui.createErrorPanel(gui.g, err.Error())
return err
}
return nil
}
if !file.HasInlineMergeConflicts {
if !file.HasMergeConflicts {
return gui.createErrorPanel(g, gui.Tr.SLocalize("FileNoMergeCons"))
}
if err := gui.changeContext("main", "merging"); err != nil {
return err
}
if err := gui.switchFocus(g, v, gui.getMainView()); err != nil {
return err
}
return gui.refreshMergePanel()
gui.switchFocus(g, v, mergeView)
return gui.refreshMergePanel(g)
}
func (gui *Gui) handleAbortMerge(g *gocui.Gui, v *gocui.View) error {
@@ -473,200 +398,14 @@ func (gui *Gui) handleAbortMerge(g *gocui.Gui, v *gocui.View) error {
}
gui.createMessagePanel(g, v, "", gui.Tr.SLocalize("MergeAborted"))
gui.refreshStatus(g)
return gui.refreshFiles()
return gui.refreshFiles(g)
}
func (gui *Gui) openFile(filename string) error {
if err := gui.OSCommand.OpenFile(filename); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return nil
}
func (gui *Gui) anyFilesWithMergeConflicts() bool {
for _, file := range gui.State.Files {
if file.HasMergeConflicts {
return true
func (gui *Gui) handleResetHard(g *gocui.Gui, v *gocui.View) error {
return gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("ClearFilePanel"), gui.Tr.SLocalize("SureResetHardHead"), func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.ResetHard(); err != nil {
gui.createErrorPanel(g, err.Error())
}
}
return false
}
type discardOption struct {
handler func(fileName *commands.File) error
description string
}
type discardAllOption struct {
handler func() error
description string
command string
}
// GetDisplayStrings is a function.
func (r *discardOption) GetDisplayStrings(isFocused bool) []string {
return []string{r.description}
}
// GetDisplayStrings is a function.
func (r *discardAllOption) GetDisplayStrings(isFocused bool) []string {
return []string{r.description, color.New(color.FgRed).Sprint(r.command)}
}
func (gui *Gui) handleCreateDiscardMenu(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
}
return nil
}
options := []*discardOption{
{
description: gui.Tr.SLocalize("discardAllChanges"),
handler: func(file *commands.File) error {
return gui.GitCommand.DiscardAllFileChanges(file)
},
},
{
description: gui.Tr.SLocalize("cancel"),
handler: func(file *commands.File) error {
return nil
},
},
}
if file.HasStagedChanges && file.HasUnstagedChanges {
discardUnstagedChanges := &discardOption{
description: gui.Tr.SLocalize("discardUnstagedChanges"),
handler: func(file *commands.File) error {
return gui.GitCommand.DiscardUnstagedFileChanges(file)
},
}
options = append(options[:1], append([]*discardOption{discardUnstagedChanges}, options[1:]...)...)
}
handleMenuPress := func(index int) error {
file, err := gui.getSelectedFile(g)
if err != nil {
return err
}
if err := options[index].handler(file); err != nil {
return err
}
return gui.refreshFiles()
}
return gui.createMenu(file.Name, options, len(options), handleMenuPress)
}
func (gui *Gui) handleCreateResetMenu(g *gocui.Gui, v *gocui.View) error {
options := []*discardAllOption{
{
description: gui.Tr.SLocalize("discardAllChangesToAllFiles"),
command: "reset --hard HEAD && git clean -fd",
handler: func() error {
return gui.GitCommand.ResetAndClean()
},
},
{
description: gui.Tr.SLocalize("discardAnyUnstagedChanges"),
command: "git checkout -- .",
handler: func() error {
return gui.GitCommand.DiscardAnyUnstagedFileChanges()
},
},
{
description: gui.Tr.SLocalize("discardUntrackedFiles"),
command: "git clean -fd",
handler: func() error {
return gui.GitCommand.RemoveUntrackedFiles()
},
},
{
description: gui.Tr.SLocalize("softReset"),
command: "git reset --soft HEAD",
handler: func() error {
return gui.GitCommand.ResetSoftHead()
},
},
{
description: gui.Tr.SLocalize("hardReset"),
command: "git reset --hard HEAD",
handler: func() error {
return gui.GitCommand.ResetHardHead()
},
},
{
description: gui.Tr.SLocalize("cancel"),
handler: func() error {
return nil
},
},
}
handleMenuPress := func(index int) error {
if err := options[index].handler(); err != nil {
return err
}
return gui.refreshFiles()
}
return gui.createMenu("", options, len(options), handleMenuPress)
}
func (gui *Gui) handleCustomCommand(g *gocui.Gui, v *gocui.View) error {
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("CustomCommand"), func(g *gocui.Gui, v *gocui.View) error {
command := gui.trimmedContent(v)
gui.SubProcess = gui.OSCommand.RunCustomCommand(command)
return gui.Errors.ErrSubProcess
})
}
type stashOption struct {
description string
handler func() error
}
// GetDisplayStrings is a function.
func (o *stashOption) GetDisplayStrings(isFocused bool) []string {
return []string{o.description}
}
func (gui *Gui) handleCreateStashMenu(g *gocui.Gui, v *gocui.View) error {
options := []*stashOption{
{
description: gui.Tr.SLocalize("stashAllChanges"),
handler: func() error {
return gui.handleStashSave(gui.GitCommand.StashSave)
},
},
{
description: gui.Tr.SLocalize("stashStagedChanges"),
handler: func() error {
return gui.handleStashSave(gui.GitCommand.StashSaveStagedChanges)
},
},
{
description: gui.Tr.SLocalize("cancel"),
handler: func() error {
return nil
},
},
}
handleMenuPress := func(index int) error {
return options[index].handler()
}
return gui.createMenu(gui.Tr.SLocalize("stashOptions"), options, len(options), handleMenuPress)
}
func (gui *Gui) handleStashChanges(g *gocui.Gui, v *gocui.View) error {
return gui.handleStashSave(gui.GitCommand.StashSave)
return gui.refreshFiles(g)
}, nil)
}
+144 -537
View File
@@ -1,33 +1,26 @@
package gui
import (
"fmt"
"io/ioutil"
"math"
"os"
"sync"
// "io"
// "io/ioutil"
"errors"
"io/ioutil"
"log"
"os"
"os/exec"
"strings"
"time"
"github.com/go-errors/errors"
// "strings"
"github.com/fatih/color"
"github.com/Sirupsen/logrus"
"github.com/golang-collections/collections/stack"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/updates"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
// OverlappingEdges determines if panel edges overlap
@@ -38,7 +31,6 @@ var OverlappingEdges = false
type SentinelErrors struct {
ErrSubProcess error
ErrNoFiles error
ErrSwitchRepo error
}
// GenerateSentinelErrors makes the sentinel errors for the gui. We're defining it here
@@ -55,7 +47,6 @@ func (gui *Gui) GenerateSentinelErrors() {
gui.Errors = SentinelErrors{
ErrSubProcess: errors.New(gui.Tr.SLocalize("RunningSubprocess")),
ErrNoFiles: errors.New(gui.Tr.SLocalize("NoChangedFiles")),
ErrSwitchRepo: errors.New("switching repo"),
}
}
@@ -64,127 +55,52 @@ type Teml i18n.Teml
// Gui wraps the gocui Gui object which handles rendering and events
type Gui struct {
g *gocui.Gui
Log *logrus.Entry
GitCommand *commands.GitCommand
OSCommand *commands.OSCommand
SubProcess *exec.Cmd
State guiState
Config config.AppConfigurer
Tr *i18n.Localizer
Errors SentinelErrors
Updater *updates.Updater
statusManager *statusManager
credentials credentials
waitForIntro sync.WaitGroup
}
// for now the staging panel state, unlike the other panel states, is going to be
// non-mutative, so that we don't accidentally end up
// with mismatches of data. We might change this in the future
type stagingPanelState struct {
SelectedLine int
StageableLines []int
HunkStarts []int
Diff string
}
type mergingPanelState struct {
ConflictIndex int
ConflictTop bool
Conflicts []commands.Conflict
EditHistory *stack.Stack
}
type filePanelState struct {
SelectedLine int
}
type branchPanelState struct {
SelectedLine int
}
type commitPanelState struct {
SelectedLine int
SpecificDiffMode bool
}
type stashPanelState struct {
SelectedLine int
}
type menuPanelState struct {
SelectedLine int
}
type commitFilesPanelState struct {
SelectedLine int
}
type panelStates struct {
Files *filePanelState
Branches *branchPanelState
Commits *commitPanelState
Stash *stashPanelState
Menu *menuPanelState
Staging *stagingPanelState
Merging *mergingPanelState
CommitFiles *commitFilesPanelState
g *gocui.Gui
Log *logrus.Logger
GitCommand *commands.GitCommand
OSCommand *commands.OSCommand
SubProcess *exec.Cmd
State guiState
Config config.AppConfigurer
Tr *i18n.Localizer
Errors SentinelErrors
}
type guiState struct {
Files []*commands.File
Branches []*commands.Branch
Commits []*commands.Commit
StashEntries []*commands.StashEntry
CommitFiles []*commands.CommitFile
DiffEntries []*commands.Commit
MenuItemCount int // can't store the actual list because it's of interface{} type
PreviousView string
Platform commands.Platform
Updating bool
Panels *panelStates
WorkingTreeState string // one of "merging", "rebasing", "normal"
Contexts map[string]string
CherryPickedCommits []*commands.Commit
Files []commands.File
Branches []commands.Branch
Commits []commands.Commit
StashEntries []commands.StashEntry
PreviousView string
HasMergeConflicts bool
ConflictIndex int
ConflictTop bool
Conflicts []commands.Conflict
EditHistory *stack.Stack
Platform commands.Platform
}
// NewGui builds a new gui handler
func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *commands.OSCommand, tr *i18n.Localizer, config config.AppConfigurer, updater *updates.Updater) (*Gui, error) {
func NewGui(log *logrus.Logger, gitCommand *commands.GitCommand, oSCommand *commands.OSCommand, tr *i18n.Localizer, config config.AppConfigurer) (*Gui, error) {
initialState := guiState{
Files: make([]*commands.File, 0),
PreviousView: "files",
Commits: make([]*commands.Commit, 0),
CherryPickedCommits: make([]*commands.Commit, 0),
StashEntries: make([]*commands.StashEntry, 0),
DiffEntries: make([]*commands.Commit, 0),
Platform: *oSCommand.Platform,
Panels: &panelStates{
Files: &filePanelState{SelectedLine: -1},
Branches: &branchPanelState{SelectedLine: 0},
Commits: &commitPanelState{SelectedLine: -1},
CommitFiles: &commitFilesPanelState{SelectedLine: -1},
Stash: &stashPanelState{SelectedLine: -1},
Menu: &menuPanelState{SelectedLine: 0},
Merging: &mergingPanelState{
ConflictIndex: 0,
ConflictTop: true,
Conflicts: []commands.Conflict{},
EditHistory: stack.New(),
},
},
Files: make([]commands.File, 0),
PreviousView: "files",
Commits: make([]commands.Commit, 0),
StashEntries: make([]commands.StashEntry, 0),
ConflictIndex: 0,
ConflictTop: true,
Conflicts: make([]commands.Conflict, 0),
EditHistory: stack.New(),
Platform: *oSCommand.Platform,
}
gui := &Gui{
Log: log,
GitCommand: gitCommand,
OSCommand: oSCommand,
State: initialState,
Config: config,
Tr: tr,
Updater: updater,
statusManager: &statusManager{},
Log: log,
GitCommand: gitCommand,
OSCommand: oSCommand,
State: initialState,
Config: config,
Tr: tr,
}
gui.GenerateSentinelErrors()
@@ -195,19 +111,16 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *comma
func (gui *Gui) scrollUpMain(g *gocui.Gui, v *gocui.View) error {
mainView, _ := g.View("main")
ox, oy := mainView.Origin()
newOy := int(math.Max(0, float64(oy-gui.Config.GetUserConfig().GetInt("gui.scrollHeight"))))
return mainView.SetOrigin(ox, newOy)
if oy >= 1 {
return mainView.SetOrigin(ox, oy-gui.Config.GetUserConfig().GetInt("gui.scrollHeight"))
}
return nil
}
func (gui *Gui) scrollDownMain(g *gocui.Gui, v *gocui.View) error {
mainView, _ := g.View("main")
ox, oy := mainView.Origin()
y := oy
if !gui.Config.GetUserConfig().GetBool("gui.scrollPastBottom") {
_, sy := mainView.Size()
y += sy
}
if y < len(mainView.BufferLines()) {
if oy < len(mainView.BufferLines()) {
return mainView.SetOrigin(ox, oy+gui.Config.GetUserConfig().GetInt("gui.scrollHeight"))
}
return nil
@@ -224,418 +137,177 @@ func max(a, b int) int {
return b
}
// getFocusLayout returns a manager function for when view gain and lose focus
func (gui *Gui) getFocusLayout() func(g *gocui.Gui) error {
var previousView *gocui.View
return func(g *gocui.Gui) error {
newView := gui.g.CurrentView()
if err := gui.onFocusChange(); err != nil {
return err
}
// for now we don't consider losing focus to a popup panel as actually losing focus
if newView != previousView && !gui.isPopupPanel(newView.Name()) {
if err := gui.onFocusLost(previousView, newView); err != nil {
return err
}
if err := gui.onFocus(newView); err != nil {
return err
}
previousView = newView
}
return nil
}
}
func (gui *Gui) onFocusChange() error {
currentView := gui.g.CurrentView()
for _, view := range gui.g.Views() {
view.Highlight = view == currentView
}
return gui.setMainTitle()
}
func (gui *Gui) onFocusLost(v *gocui.View, newView *gocui.View) error {
if v == nil {
return nil
}
if v.Name() == "branches" {
// This stops the branches panel from showing the upstream/downstream changes to the selected branch, when it loses focus
// inside renderListPanel it checks to see if the panel has focus
if err := gui.renderListPanel(gui.getBranchesView(), gui.State.Branches); err != nil {
return err
}
} else if v.Name() == "main" {
// if we have lost focus to a first-class panel, we need to do some cleanup
if err := gui.changeContext("main", "normal"); err != nil {
return err
}
} else if v.Name() == "commitFiles" {
if _, err := gui.g.SetViewOnBottom(v.Name()); err != nil {
return err
}
}
gui.Log.Info(v.Name() + " focus lost")
return nil
}
func (gui *Gui) onFocus(v *gocui.View) error {
if v == nil {
return nil
}
gui.Log.Info(v.Name() + " focus gained")
return nil
}
// layout is called for every screen re-render e.g. when the screen is resized
func (gui *Gui) layout(g *gocui.Gui) error {
g.Highlight = true
width, height := g.Size()
information := gui.Config.GetVersion()
if gui.g.Mouse {
donate := color.New(color.FgMagenta, color.Underline).Sprint(gui.Tr.SLocalize("Donate"))
information = donate + " " + information
}
minimumHeight := 9
minimumWidth := 10
if height < minimumHeight || width < minimumWidth {
v, err := g.SetView("limit", 0, 0, width-1, height-1, 0)
if err != nil {
if err.Error() != "unknown view" {
return err
}
v.Title = gui.Tr.SLocalize("NotEnoughSpace")
v.Wrap = true
_, _ = g.SetViewOnTop("limit")
}
return nil
}
currView := gui.g.CurrentView()
currentCyclebleView := gui.State.PreviousView
if currView != nil {
viewName := currView.Name()
usePreviouseView := true
for _, view := range cyclableViews {
if view == viewName {
currentCyclebleView = viewName
usePreviouseView = false
break
}
}
if usePreviouseView {
currentCyclebleView = gui.State.PreviousView
}
}
usableSpace := height - 7
extraSpace := usableSpace - (usableSpace/3)*3
vHeights := map[string]int{
"status": 3,
"files": (usableSpace / 3) + extraSpace,
"branches": usableSpace / 3,
"commits": usableSpace / 3,
"stash": 3,
"options": 1,
}
if height < 28 {
defaultHeight := 3
if height < 21 {
defaultHeight = 1
}
vHeights = map[string]int{
"status": defaultHeight,
"files": defaultHeight,
"branches": defaultHeight,
"commits": defaultHeight,
"stash": defaultHeight,
"options": defaultHeight,
}
vHeights[currentCyclebleView] = height - defaultHeight*4 - 1
}
optionsVersionBoundary := width - max(len(utils.Decolorise(information)), 1)
leftSideWidth := width / 3
appStatus := gui.statusManager.getStatusString()
appStatusOptionsBoundary := 0
if appStatus != "" {
appStatusOptionsBoundary = len(appStatus) + 2
}
statusFilesBoundary := 2
filesBranchesBoundary := 2 * height / 5 // height - 20
commitsBranchesBoundary := 3 * height / 5 // height - 10
commitsStashBoundary := height - 5 // height - 5
minimumHeight := 16
minimumWidth := 10
version := gui.Config.GetVersion()
panelSpacing := 1
if OverlappingEdges {
panelSpacing = 0
}
_, _ = g.SetViewOnBottom("limit")
if height < minimumHeight || width < minimumWidth {
v, err := g.SetView("limit", 0, 0, max(width-1, 2), max(height-1, 2), 0)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = gui.Tr.SLocalize("NotEnoughSpace")
v.Wrap = true
}
return nil
}
g.DeleteView("limit")
textColor := theme.GocuiDefaultTextColor
v, err := g.SetView("main", leftSideWidth+panelSpacing, 0, width-1, height-2, gocui.LEFT)
optionsTop := height - 2
// hiding options if there's not enough space
if height < 30 {
optionsTop = height - 1
}
v, err := g.SetView("main", leftSideWidth+panelSpacing, 0, width-1, optionsTop, gocui.LEFT)
if err != nil {
if err.Error() != "unknown view" {
if err != gocui.ErrUnknownView {
return err
}
v.Title = gui.Tr.SLocalize("DiffTitle")
v.Wrap = true
v.FgColor = textColor
v.FgColor = gocui.ColorWhite
}
if v, err := g.SetView("status", 0, 0, leftSideWidth, vHeights["status"]-1, gocui.BOTTOM|gocui.RIGHT); err != nil {
if err.Error() != "unknown view" {
if v, err := g.SetView("status", 0, 0, leftSideWidth, statusFilesBoundary, gocui.BOTTOM|gocui.RIGHT); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = gui.Tr.SLocalize("StatusTitle")
v.FgColor = textColor
v.FgColor = gocui.ColorWhite
}
filesView, err := g.SetViewBeneath("files", "status", vHeights["files"])
filesView, err := g.SetView("files", 0, statusFilesBoundary+panelSpacing, leftSideWidth, filesBranchesBoundary, gocui.TOP|gocui.BOTTOM)
if err != nil {
if err.Error() != "unknown view" {
if err != gocui.ErrUnknownView {
return err
}
filesView.Highlight = true
filesView.Title = gui.Tr.SLocalize("FilesTitle")
v.FgColor = textColor
v.FgColor = gocui.ColorWhite
}
branchesView, err := g.SetViewBeneath("branches", "files", vHeights["branches"])
if err != nil {
if err.Error() != "unknown view" {
if v, err := g.SetView("branches", 0, filesBranchesBoundary+panelSpacing, leftSideWidth, commitsBranchesBoundary, gocui.TOP|gocui.BOTTOM); err != nil {
if err != gocui.ErrUnknownView {
return err
}
branchesView.Title = gui.Tr.SLocalize("BranchesTitle")
branchesView.FgColor = textColor
v.Title = gui.Tr.SLocalize("BranchesTitle")
v.FgColor = gocui.ColorWhite
}
if v, err := g.SetViewBeneath("commitFiles", "branches", vHeights["commits"]); err != nil {
if err.Error() != "unknown view" {
if v, err := g.SetView("commits", 0, commitsBranchesBoundary+panelSpacing, leftSideWidth, commitsStashBoundary, gocui.TOP|gocui.BOTTOM); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = gui.Tr.SLocalize("CommitFiles")
v.FgColor = textColor
v.Title = gui.Tr.SLocalize("CommitsTitle")
v.FgColor = gocui.ColorWhite
}
commitsView, err := g.SetViewBeneath("commits", "branches", vHeights["commits"])
if err != nil {
if err.Error() != "unknown view" {
if v, err := g.SetView("stash", 0, commitsStashBoundary+panelSpacing, leftSideWidth, optionsTop, gocui.TOP|gocui.RIGHT); err != nil {
if err != gocui.ErrUnknownView {
return err
}
commitsView.Title = gui.Tr.SLocalize("CommitsTitle")
commitsView.FgColor = textColor
v.Title = gui.Tr.SLocalize("StashTitle")
v.FgColor = gocui.ColorWhite
}
stashView, err := g.SetViewBeneath("stash", "commits", vHeights["stash"])
if err != nil {
if err.Error() != "unknown view" {
return err
}
stashView.Title = gui.Tr.SLocalize("StashTitle")
stashView.FgColor = textColor
}
if v, err := g.SetView("options", appStatusOptionsBoundary-1, height-2, optionsVersionBoundary-1, height, 0); err != nil {
if err.Error() != "unknown view" {
if v, err := g.SetView("options", -1, optionsTop, width-len(version)-2, optionsTop+2, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Frame = false
userConfig := gui.Config.GetUserConfig()
v.FgColor = theme.GetColor(userConfig.GetStringSlice("gui.theme.optionsTextColor"))
if v.FgColor, err = gui.GetOptionsPanelTextColor(); err != nil {
return err
}
}
if gui.getCommitMessageView() == nil {
if gui.getCommitMessageView(g) == nil {
// doesn't matter where this view starts because it will be hidden
if commitMessageView, err := g.SetView("commitMessage", width, height, width*2, height*2, 0); err != nil {
if err.Error() != "unknown view" {
if commitMessageView, err := g.SetView("commitMessage", 0, 0, width, height, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
g.SetViewOnBottom("commitMessage")
commitMessageView.Title = gui.Tr.SLocalize("CommitMessage")
commitMessageView.FgColor = textColor
commitMessageView.FgColor = gocui.ColorWhite
commitMessageView.Editable = true
}
}
if check, _ := g.View("credentials"); check == nil {
// doesn't matter where this view starts because it will be hidden
if credentialsView, err := g.SetView("credentials", width, height, width*2, height*2, 0); err != nil {
if err.Error() != "unknown view" {
return err
}
_, err := g.SetViewOnBottom("credentials")
if err != nil {
return err
}
credentialsView.Title = gui.Tr.SLocalize("CredentialsUsername")
credentialsView.FgColor = textColor
credentialsView.Editable = true
}
}
if appStatusView, err := g.SetView("appStatus", -1, height-2, width, height, 0); err != nil {
if err.Error() != "unknown view" {
return err
}
appStatusView.BgColor = gocui.ColorDefault
appStatusView.FgColor = gocui.ColorCyan
appStatusView.Frame = false
if _, err := g.SetViewOnBottom("appStatus"); err != nil {
return err
}
}
if v, err := g.SetView("information", optionsVersionBoundary-1, height-2, width, height, 0); err != nil {
if err.Error() != "unknown view" {
if v, err := g.SetView("version", width-len(version)-1, optionsTop, width, optionsTop+2, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.BgColor = gocui.ColorDefault
v.FgColor = gocui.ColorGreen
v.Frame = false
if err := gui.renderString(g, "information", information); err != nil {
if err := gui.renderString(g, "version", version); err != nil {
return err
}
// doing this here because it'll only happen once
if err := gui.loadNewRepo(); err != nil {
// these are only called once
gui.handleFileSelect(g, filesView)
gui.refreshFiles(g)
gui.refreshBranches(g)
gui.refreshCommits(g)
gui.refreshStashEntries(g)
if err := gui.switchFocus(g, nil, filesView); err != nil {
return err
}
}
if gui.g.CurrentView() == nil {
if _, err := gui.g.SetCurrentView(gui.getFilesView().Name()); err != nil {
return err
}
gui.resizePopupPanels(g)
if err := gui.switchFocus(gui.g, nil, gui.getFilesView()); err != nil {
return err
}
}
type listViewState struct {
selectedLine int
lineCount int
}
listViews := map[*gocui.View]listViewState{
filesView: {selectedLine: gui.State.Panels.Files.SelectedLine, lineCount: len(gui.State.Files)},
branchesView: {selectedLine: gui.State.Panels.Branches.SelectedLine, lineCount: len(gui.State.Branches)},
commitsView: {selectedLine: gui.State.Panels.Commits.SelectedLine, lineCount: len(gui.State.Commits)},
stashView: {selectedLine: gui.State.Panels.Stash.SelectedLine, lineCount: len(gui.State.StashEntries)},
}
// menu view might not exist so we check to be safe
if menuView, err := gui.g.View("menu"); err == nil {
listViews[menuView] = listViewState{selectedLine: gui.State.Panels.Menu.SelectedLine, lineCount: gui.State.MenuItemCount}
}
for view, state := range listViews {
// check if the selected line is now out of view and if so refocus it
if err := gui.focusPoint(0, state.selectedLine, state.lineCount, view); err != nil {
return err
}
}
// here is a good place log some stuff
// if you download humanlog and do tail -f development.log | humanlog
// this will let you see these branches as prettified json
// gui.Log.Info(utils.AsJson(gui.State.Branches[0:4]))
return gui.resizeCurrentPopupPanel(g)
}
func (gui *Gui) loadNewRepo() error {
gui.Updater.CheckForNewUpdate(gui.onBackgroundUpdateCheckFinish, false)
if err := gui.updateRecentRepoList(); err != nil {
return err
}
gui.waitForIntro.Done()
if err := gui.refreshSidePanels(gui.g); err != nil {
return err
}
if gui.Config.GetUserConfig().GetString("reporting") == "undetermined" {
if err := gui.promptAnonymousReporting(); err != nil {
return err
}
}
return nil
}
func (gui *Gui) promptAnonymousReporting() error {
return gui.createConfirmationPanel(gui.g, nil, gui.Tr.SLocalize("AnonymousReportingTitle"), gui.Tr.SLocalize("AnonymousReportingPrompt"), func(g *gocui.Gui, v *gocui.View) error {
gui.waitForIntro.Done()
return gui.Config.WriteToUserConfig("reporting", "on")
}, func(g *gocui.Gui, v *gocui.View) error {
gui.waitForIntro.Done()
return gui.Config.WriteToUserConfig("reporting", "off")
})
}
func (gui *Gui) fetch(g *gocui.Gui, v *gocui.View, canAskForCredentials bool) (unamePassOpend bool, err error) {
unamePassOpend = false
err = gui.GitCommand.Fetch(func(passOrUname string) string {
unamePassOpend = true
return gui.waitForPassUname(gui.g, v, passOrUname)
}, canAskForCredentials)
if canAskForCredentials && err != nil && strings.Contains(err.Error(), "exit status 128") {
colorFunction := color.New(color.FgRed).SprintFunc()
coloredMessage := colorFunction(strings.TrimSpace(gui.Tr.SLocalize("PassUnameWrong")))
close := func(g *gocui.Gui, v *gocui.View) error {
return nil
}
_ = gui.createConfirmationPanel(g, v, gui.Tr.SLocalize("Error"), coloredMessage, close, close)
}
func (gui *Gui) fetch(g *gocui.Gui) error {
gui.GitCommand.Fetch()
gui.refreshStatus(g)
return unamePassOpend, err
return nil
}
func (gui *Gui) renderAppStatus() error {
appStatus := gui.statusManager.getStatusString()
if appStatus != "" {
return gui.renderString(gui.g, "appStatus", appStatus)
func (gui *Gui) updateLoader(g *gocui.Gui) error {
if confirmationView, _ := g.View("confirmation"); confirmationView != nil {
content := gui.trimmedContent(confirmationView)
if strings.Contains(content, "...") {
staticContent := strings.Split(content, "...")[0] + "..."
gui.renderString(g, "confirmation", staticContent+" "+gui.loader())
}
}
return nil
}
func (gui *Gui) renderGlobalOptions() error {
return gui.renderOptionsMap(map[string]string{
"PgUp/PgDn": gui.Tr.SLocalize("scroll"),
"← → ↑ ↓": gui.Tr.SLocalize("navigate"),
"esc/q": gui.Tr.SLocalize("close"),
"x": gui.Tr.SLocalize("menu"),
"1-5": gui.Tr.SLocalize("jump"),
})
}
func (gui *Gui) goEvery(interval time.Duration, function func() error) {
func (gui *Gui) goEvery(g *gocui.Gui, interval time.Duration, function func(*gocui.Gui) error) {
go func() {
for range time.Tick(interval) {
_ = function()
function(g)
}
}()
}
func (gui *Gui) startBackgroundFetch() {
gui.waitForIntro.Wait()
isNew := gui.Config.GetIsNewRepo()
if !isNew {
time.After(60 * time.Second)
}
_, err := gui.fetch(gui.g, gui.g.CurrentView(), false)
if err != nil && strings.Contains(err.Error(), "exit status 128") && isNew {
_ = gui.createConfirmationPanel(gui.g, gui.g.CurrentView(), gui.Tr.SLocalize("NoAutomaticGitFetchTitle"), gui.Tr.SLocalize("NoAutomaticGitFetchBody"), nil, nil)
} else {
gui.goEvery(time.Second*60, func() error {
_, err := gui.fetch(gui.g, gui.g.CurrentView(), false)
return err
})
func (gui *Gui) resizePopupPanels(g *gocui.Gui) error {
v := g.CurrentView()
if v.Name() == "commitMessage" || v.Name() == "confirmation" {
return gui.resizePopupPanel(g, v)
}
return nil
}
// Run setup the gui with keybindings and start the mainloop
@@ -646,29 +318,17 @@ func (gui *Gui) Run() error {
}
defer g.Close()
if gui.Config.GetUserConfig().GetBool("gui.mouseEvents") {
g.Mouse = true
}
gui.g = g // TODO: always use gui.g rather than passing g around everywhere
if err := gui.setColorScheme(); err != nil {
if err := gui.SetColorScheme(); err != nil {
return err
}
if gui.Config.GetUserConfig().GetString("reporting") == "undetermined" {
gui.waitForIntro.Add(2)
} else {
gui.waitForIntro.Add(1)
}
gui.goEvery(g, time.Second*60, gui.fetch)
gui.goEvery(g, time.Second*10, gui.refreshFiles)
gui.goEvery(g, time.Millisecond*10, gui.updateLoader)
if gui.Config.GetUserConfig().GetBool("git.autoFetch") {
go gui.startBackgroundFetch()
}
gui.goEvery(time.Second*10, gui.refreshFiles)
gui.goEvery(time.Millisecond*50, gui.renderAppStatus)
g.SetManager(gocui.ManagerFunc(gui.layout), gocui.ManagerFunc(gui.getFocusLayout()))
g.SetManagerFunc(gui.layout)
if err = gui.keybindings(g); err != nil {
return err
@@ -681,80 +341,27 @@ func (gui *Gui) Run() error {
// RunWithSubprocesses loops, instantiating a new gocui.Gui with each iteration
// if the error returned from a run is a ErrSubProcess, it runs the subprocess
// otherwise it handles the error, possibly by quitting the application
func (gui *Gui) RunWithSubprocesses() error {
func (gui *Gui) RunWithSubprocesses() {
for {
if err := gui.Run(); err != nil {
if err == gocui.ErrQuit {
break
} else if err == gui.Errors.ErrSwitchRepo {
continue
} else if err == gui.Errors.ErrSubProcess {
if err := gui.runCommand(); err != nil {
return err
}
gui.SubProcess.Stdin = os.Stdin
gui.SubProcess.Stdout = os.Stdout
gui.SubProcess.Stderr = os.Stderr
gui.SubProcess.Run()
gui.SubProcess.Stdout = ioutil.Discard
gui.SubProcess.Stderr = ioutil.Discard
gui.SubProcess.Stdin = nil
gui.SubProcess = nil
} else {
return err
log.Panicln(err)
}
}
}
return nil
}
func (gui *Gui) runCommand() error {
gui.SubProcess.Stdout = os.Stdout
gui.SubProcess.Stderr = os.Stdout
gui.SubProcess.Stdin = os.Stdin
fmt.Fprintf(os.Stdout, "\n%s\n\n", utils.ColoredString("+ "+strings.Join(gui.SubProcess.Args, " "), color.FgBlue))
if err := gui.SubProcess.Run(); err != nil {
// not handling the error explicitly because usually we're going to see it
// in the output anyway
gui.Log.Error(err)
}
gui.SubProcess.Stdout = ioutil.Discard
gui.SubProcess.Stderr = ioutil.Discard
gui.SubProcess.Stdin = nil
gui.SubProcess = nil
fmt.Fprintf(os.Stdout, "\n%s", utils.ColoredString(gui.Tr.SLocalize("pressEnterToReturn"), color.FgGreen))
fmt.Scanln() // wait for enter press
return nil
}
func (gui *Gui) quit(g *gocui.Gui, v *gocui.View) error {
if gui.State.Updating {
return gui.createUpdateQuitConfirmation(g, v)
}
if gui.Config.GetUserConfig().GetBool("confirmOnQuit") {
return gui.createConfirmationPanel(g, v, "", gui.Tr.SLocalize("ConfirmQuit"), func(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}, nil)
}
return gocui.ErrQuit
}
func (gui *Gui) handleDonate(g *gocui.Gui, v *gocui.View) error {
if !gui.g.Mouse {
return nil
}
cx, _ := v.Cursor()
if cx > len(gui.Tr.SLocalize("Donate")) {
return nil
}
return gui.OSCommand.OpenLink("https://donorbox.org/lazygit")
}
// setColorScheme sets the color scheme for the app based on the user config
func (gui *Gui) setColorScheme() error {
userConfig := gui.Config.GetUserConfig()
theme.UpdateTheme(userConfig)
gui.g.FgColor = theme.InactiveBorderColor
gui.g.SelFgColor = theme.ActiveBorderColor
return nil
}
+77 -758
View File
@@ -1,776 +1,95 @@
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
// is only handled if the given view has focus, or handled globally if the view
// is ""
type Binding struct {
ViewName string
Handler func(*gocui.Gui, *gocui.View) error
Key interface{} // FIXME: find out how to get `gocui.Key | rune`
Modifier gocui.Modifier
Description string
Alternative string
}
// GetDisplayStrings returns the display string of a file
func (b *Binding) GetDisplayStrings(isFocused bool) []string {
return []string{b.GetKey(), b.Description}
}
// GetKey is a function.
func (b *Binding) GetKey() string {
key := 0
switch b.Key.(type) {
case rune:
key = int(b.Key.(rune))
case gocui.Key:
if b.Key.(gocui.Key) == gocui.KeyCtrlJ {
return "ctrl+j"
}
if b.Key.(gocui.Key) == gocui.KeyCtrlK {
return "ctrl+k"
}
key = int(b.Key.(gocui.Key))
}
// special keys
switch key {
case 27:
return "esc"
case 13:
return "enter"
case 32:
return "space"
case 65514:
return "►"
case 65515:
return "◄"
case 65517:
return "▲"
case 65516:
return "▼"
case 65508:
return "PgUp"
case 65507:
return "PgDn"
}
return string(key)
}
// GetInitialKeybindings is a function.
func (gui *Gui) GetInitialKeybindings() []*Binding {
bindings := []*Binding{
{
ViewName: "",
Key: 'q',
Modifier: gocui.ModNone,
Handler: gui.quit,
}, {
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,
Alternative: "fn+up",
}, {
ViewName: "",
Key: gocui.KeyPgdn,
Modifier: gocui.ModNone,
Handler: gui.scrollDownMain,
Alternative: "fn+down",
}, {
ViewName: "",
Key: 'K',
Modifier: gocui.ModNone,
Handler: gui.scrollUpMain,
}, {
ViewName: "",
Key: 'J',
Modifier: gocui.ModNone,
Handler: gui.scrollDownMain,
}, {
ViewName: "",
Key: gocui.KeyCtrlU,
Modifier: gocui.ModNone,
Handler: gui.scrollUpMain,
}, {
ViewName: "",
Key: gocui.KeyCtrlD,
Modifier: gocui.ModNone,
Handler: gui.scrollDownMain,
}, {
ViewName: "",
Key: 'm',
Modifier: gocui.ModNone,
Handler: gui.handleCreateRebaseOptionsMenu,
Description: gui.Tr.SLocalize("ViewMergeRebaseOptions"),
}, {
ViewName: "",
Key: 'P',
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.handleCreateRecentReposMenu,
Description: gui.Tr.SLocalize("SwitchRepo"),
},
{
ViewName: "files",
Key: 'c',
Modifier: gocui.ModNone,
Handler: gui.handleCommitPress,
Description: gui.Tr.SLocalize("CommitChanges"),
},
{
ViewName: "files",
Key: 'w',
Modifier: gocui.ModNone,
Handler: gui.handleWIPCommitPress,
Description: gui.Tr.SLocalize("commitChangesWithoutHook"),
}, {
ViewName: "files",
Key: 'A',
Modifier: gocui.ModNone,
Handler: gui.handleAmendCommitPress,
Description: gui.Tr.SLocalize("AmendLastCommit"),
}, {
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,
Description: gui.Tr.SLocalize("toggleStaged"),
}, {
ViewName: "files",
Key: 'd',
Modifier: gocui.ModNone,
Handler: gui.handleCreateDiscardMenu,
Description: gui.Tr.SLocalize("viewDiscardOptions"),
}, {
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.handleStashChanges,
Description: gui.Tr.SLocalize("stashAllChanges"),
}, {
ViewName: "files",
Key: 'S',
Modifier: gocui.ModNone,
Handler: gui.handleCreateStashMenu,
Description: gui.Tr.SLocalize("viewStashOptions"),
}, {
ViewName: "files",
Key: 'a',
Modifier: gocui.ModNone,
Handler: gui.handleStageAll,
Description: gui.Tr.SLocalize("toggleStagedAll"),
}, {
ViewName: "files",
Key: 't',
Modifier: gocui.ModNone,
Handler: gui.handleAddPatch,
Description: gui.Tr.SLocalize("addPatch"),
}, {
ViewName: "files",
Key: 'D',
Modifier: gocui.ModNone,
Handler: gui.handleCreateResetMenu,
Description: gui.Tr.SLocalize("viewResetOptions"),
}, {
ViewName: "files",
Key: gocui.KeyEnter,
Modifier: gocui.ModNone,
Handler: gui.handleEnterFile,
Description: gui.Tr.SLocalize("StageLines"),
}, {
ViewName: "files",
Key: 'f',
Modifier: gocui.ModNone,
Handler: gui.handleGitFetch,
Description: gui.Tr.SLocalize("fetch"),
}, {
ViewName: "files",
Key: 'X',
Modifier: gocui.ModNone,
Handler: gui.handleCustomCommand,
Description: gui.Tr.SLocalize("executeCustomCommand"),
}, {
ViewName: "branches",
Key: gocui.KeySpace,
Modifier: gocui.ModNone,
Handler: gui.handleBranchPress,
Description: gui.Tr.SLocalize("checkout"),
}, {
ViewName: "branches",
Key: 'o',
Modifier: gocui.ModNone,
Handler: gui.handleCreatePullRequestPress,
Description: gui.Tr.SLocalize("createPullRequest"),
}, {
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: 'r',
Modifier: gocui.ModNone,
Handler: gui.handleRebase,
Description: gui.Tr.SLocalize("rebaseBranch"),
}, {
ViewName: "branches",
Key: 'M',
Modifier: gocui.ModNone,
Handler: gui.handleMerge,
Description: gui.Tr.SLocalize("mergeIntoCurrentBranch"),
}, {
ViewName: "branches",
Key: 'f',
Modifier: gocui.ModNone,
Handler: gui.handleFastForward,
Description: gui.Tr.SLocalize("FastForward"),
}, {
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.handleCreateCommitResetMenu,
Description: gui.Tr.SLocalize("resetToThisCommit"),
}, {
ViewName: "commits",
Key: 'f',
Modifier: gocui.ModNone,
Handler: gui.handleCommitFixup,
Description: gui.Tr.SLocalize("fixupCommit"),
}, {
ViewName: "commits",
Key: 'F',
Modifier: gocui.ModNone,
Handler: gui.handleCreateFixupCommit,
Description: gui.Tr.SLocalize("createFixupCommit"),
}, {
ViewName: "commits",
Key: 'S',
Modifier: gocui.ModNone,
Handler: gui.handleSquashAllAboveFixupCommits,
Description: gui.Tr.SLocalize("squashAboveCommits"),
}, {
ViewName: "commits",
Key: 'd',
Modifier: gocui.ModNone,
Handler: gui.handleCommitDelete,
Description: gui.Tr.SLocalize("deleteCommit"),
}, {
ViewName: "commits",
Key: gocui.KeyCtrlJ,
Modifier: gocui.ModNone,
Handler: gui.handleCommitMoveDown,
Description: gui.Tr.SLocalize("moveDownCommit"),
}, {
ViewName: "commits",
Key: gocui.KeyCtrlK,
Modifier: gocui.ModNone,
Handler: gui.handleCommitMoveUp,
Description: gui.Tr.SLocalize("moveUpCommit"),
}, {
ViewName: "commits",
Key: 'e',
Modifier: gocui.ModNone,
Handler: gui.handleCommitEdit,
Description: gui.Tr.SLocalize("editCommit"),
}, {
ViewName: "commits",
Key: 'A',
Modifier: gocui.ModNone,
Handler: gui.handleCommitAmendTo,
Description: gui.Tr.SLocalize("amendToCommit"),
}, {
ViewName: "commits",
Key: 'p',
Modifier: gocui.ModNone,
Handler: gui.handleCommitPick,
Description: gui.Tr.SLocalize("pickCommit"),
}, {
ViewName: "commits",
Key: 't',
Modifier: gocui.ModNone,
Handler: gui.handleCommitRevert,
Description: gui.Tr.SLocalize("revertCommit"),
}, {
ViewName: "commits",
Key: 'c',
Modifier: gocui.ModNone,
Handler: gui.handleCopyCommit,
Description: gui.Tr.SLocalize("cherryPickCopy"),
}, {
ViewName: "commits",
Key: 'C',
Modifier: gocui.ModNone,
Handler: gui.handleCopyCommitRange,
Description: gui.Tr.SLocalize("cherryPickCopyRange"),
}, {
ViewName: "commits",
Key: 'v',
Modifier: gocui.ModNone,
Handler: gui.HandlePasteCommits,
Description: gui.Tr.SLocalize("pasteCommits"),
}, {
ViewName: "commits",
Key: gocui.KeyEnter,
Modifier: gocui.ModNone,
Handler: gui.handleSwitchToCommitFilesPanel,
Description: gui.Tr.SLocalize("viewCommitFiles"),
}, {
ViewName: "commits",
Key: gocui.KeySpace,
Modifier: gocui.ModNone,
Handler: gui.handleToggleDiffCommit,
Description: gui.Tr.SLocalize("CommitsDiff"),
}, {
ViewName: "stash",
Key: gocui.KeySpace,
Modifier: gocui.ModNone,
Handler: gui.handleStashApply,
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: "credentials",
Key: gocui.KeyEnter,
Modifier: gocui.ModNone,
Handler: gui.handleSubmitCredential,
}, {
ViewName: "credentials",
Key: gocui.KeyEsc,
Modifier: gocui.ModNone,
Handler: gui.handleCloseCredentialsView,
}, {
ViewName: "menu",
Key: gocui.KeyEsc,
Modifier: gocui.ModNone,
Handler: gui.handleMenuClose,
}, {
ViewName: "menu",
Key: 'q',
Modifier: gocui.ModNone,
Handler: gui.handleMenuClose,
}, {
ViewName: "information",
Key: gocui.MouseLeft,
Modifier: gocui.ModNone,
Handler: gui.handleDonate,
}, {
ViewName: "commitFiles",
Key: gocui.KeyEsc,
Modifier: gocui.ModNone,
Handler: gui.handleSwitchToCommitsPanel,
Description: gui.Tr.SLocalize("goBack"),
}, {
ViewName: "commitFiles",
Key: 'c',
Modifier: gocui.ModNone,
Handler: gui.handleCheckoutCommitFile,
Description: gui.Tr.SLocalize("checkoutCommitFile"),
}, {
ViewName: "commitFiles",
Key: 'd',
Modifier: gocui.ModNone,
Handler: gui.handleDiscardOldFileChange,
Description: gui.Tr.SLocalize("discardOldFileChange"),
},
{
ViewName: "commitFiles",
Key: 'o',
Modifier: gocui.ModNone,
Handler: gui.handleOpenOldCommitFile,
Description: gui.Tr.SLocalize("openFile"),
},
}
for _, viewName := range []string{"status", "branches", "files", "commits", "commitFiles", "stash", "menu"} {
bindings = append(bindings, []*Binding{
{ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.nextView},
{ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.previousView},
{ViewName: viewName, Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: gui.nextView},
{ViewName: viewName, Key: 'h', Modifier: gocui.ModNone, Handler: gui.previousView},
{ViewName: viewName, Key: 'l', Modifier: gocui.ModNone, Handler: gui.nextView},
}...)
}
// Appends keybindings to jump to a particular sideView using numbers
for i, viewName := range []string{"status", "files", "branches", "commits", "stash"} {
bindings = append(bindings, &Binding{ViewName: "", Key: rune(i+1) + '0', Modifier: gocui.ModNone, Handler: gui.goToSideView(viewName)})
}
listPanelMap := map[string]struct {
prevLine func(*gocui.Gui, *gocui.View) error
nextLine func(*gocui.Gui, *gocui.View) error
focus func(*gocui.Gui, *gocui.View) error
}{
"menu": {prevLine: gui.handleMenuPrevLine, nextLine: gui.handleMenuNextLine, focus: gui.handleMenuSelect},
"files": {prevLine: gui.handleFilesPrevLine, nextLine: gui.handleFilesNextLine, focus: gui.handleFilesFocus},
"branches": {prevLine: gui.handleBranchesPrevLine, nextLine: gui.handleBranchesNextLine, focus: gui.handleBranchSelect},
"commits": {prevLine: gui.handleCommitsPrevLine, nextLine: gui.handleCommitsNextLine, focus: gui.handleCommitSelect},
"stash": {prevLine: gui.handleStashPrevLine, nextLine: gui.handleStashNextLine, focus: gui.handleStashEntrySelect},
"status": {focus: gui.handleStatusSelect},
"commitFiles": {prevLine: gui.handleCommitFilesPrevLine, nextLine: gui.handleCommitFilesNextLine, focus: gui.handleCommitFileSelect},
}
for viewName, functions := range listPanelMap {
bindings = append(bindings, []*Binding{
{ViewName: viewName, Key: 'k', Modifier: gocui.ModNone, Handler: functions.prevLine},
{ViewName: viewName, Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: functions.prevLine},
{ViewName: viewName, Key: gocui.MouseWheelUp, Modifier: gocui.ModNone, Handler: functions.prevLine},
{ViewName: viewName, Key: 'j', Modifier: gocui.ModNone, Handler: functions.nextLine},
{ViewName: viewName, Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: functions.nextLine},
{ViewName: viewName, Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, Handler: functions.nextLine},
{ViewName: viewName, Key: gocui.MouseLeft, Modifier: gocui.ModNone, Handler: functions.focus},
}...)
}
return bindings
}
// GetCurrentKeybindings gets the list of keybindings given the current context
func (gui *Gui) GetCurrentKeybindings() []*Binding {
bindings := gui.GetInitialKeybindings()
viewName := gui.currentViewName()
currentContext := gui.State.Contexts[viewName]
contextBindings := gui.GetContextMap()[viewName][currentContext]
return append(bindings, contextBindings...)
ViewName string
Handler func(*gocui.Gui, *gocui.View) error
Key interface{} // FIXME: find out how to get `gocui.Key | rune`
Modifier gocui.Modifier
}
func (gui *Gui) keybindings(g *gocui.Gui) error {
bindings := gui.GetInitialKeybindings()
bindings := []Binding{
{ViewName: "", Key: 'q', Modifier: gocui.ModNone, Handler: gui.quit},
{ViewName: "", Key: gocui.KeyCtrlC, Modifier: gocui.ModNone, Handler: gui.quit},
{ViewName: "", Key: gocui.KeyPgup, Modifier: gocui.ModNone, Handler: gui.scrollUpMain},
{ViewName: "", Key: gocui.KeyPgdn, Modifier: gocui.ModNone, Handler: gui.scrollDownMain},
{ViewName: "", Key: gocui.KeyCtrlU, Modifier: gocui.ModNone, Handler: gui.scrollUpMain},
{ViewName: "", Key: gocui.KeyCtrlD, Modifier: gocui.ModNone, Handler: gui.scrollDownMain},
{ViewName: "", Key: 'P', Modifier: gocui.ModNone, Handler: gui.pushFiles},
{ViewName: "", Key: 'p', Modifier: gocui.ModNone, Handler: gui.pullFiles},
{ViewName: "", Key: 'R', Modifier: gocui.ModNone, Handler: gui.handleRefresh},
{ViewName: "status", Key: 'e', Modifier: gocui.ModNone, Handler: gui.handleEditConfig},
{ViewName: "status", Key: 'o', Modifier: gocui.ModNone, Handler: gui.handleOpenConfig},
{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},
{ViewName: "files", Key: 'd', Modifier: gocui.ModNone, Handler: gui.handleFileRemove},
{ViewName: "files", Key: 'm', Modifier: gocui.ModNone, Handler: gui.handleSwitchToMerge},
{ViewName: "files", Key: 'e', Modifier: gocui.ModNone, Handler: gui.handleFileEdit},
{ViewName: "files", Key: 'o', Modifier: gocui.ModNone, Handler: gui.handleFileOpen},
{ViewName: "files", Key: 's', Modifier: gocui.ModNone, Handler: gui.handleSublimeFileOpen},
{ViewName: "files", Key: 'v', Modifier: gocui.ModNone, Handler: gui.handleVsCodeFileOpen},
{ViewName: "files", Key: 'i', Modifier: gocui.ModNone, Handler: gui.handleIgnoreFile},
{ViewName: "files", Key: 'r', Modifier: gocui.ModNone, Handler: gui.handleRefreshFiles},
{ViewName: "files", Key: 'S', Modifier: gocui.ModNone, Handler: gui.handleStashSave},
{ViewName: "files", Key: 'a', Modifier: gocui.ModNone, Handler: gui.handleAbortMerge},
{ViewName: "files", Key: 't', Modifier: gocui.ModNone, Handler: gui.handleAddPatch},
{ViewName: "files", Key: 'D', Modifier: gocui.ModNone, Handler: gui.handleResetHard},
{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},
{ViewName: "branches", Key: 'c', Modifier: gocui.ModNone, Handler: gui.handleCheckoutByName},
{ViewName: "branches", Key: 'F', Modifier: gocui.ModNone, Handler: gui.handleForceCheckout},
{ViewName: "branches", Key: 'n', Modifier: gocui.ModNone, Handler: gui.handleNewBranch},
{ViewName: "branches", Key: 'd', Modifier: gocui.ModNone, Handler: gui.handleDeleteBranch},
{ViewName: "branches", Key: 'm', Modifier: gocui.ModNone, Handler: gui.handleMerge},
{ViewName: "commits", Key: 's', Modifier: gocui.ModNone, Handler: gui.handleCommitSquashDown},
{ViewName: "commits", Key: 'r', Modifier: gocui.ModNone, Handler: gui.handleRenameCommit},
{ViewName: "commits", Key: 'g', Modifier: gocui.ModNone, Handler: gui.handleResetToCommit},
{ViewName: "commits", Key: 'f', Modifier: gocui.ModNone, Handler: gui.handleCommitFixup},
{ViewName: "stash", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handleStashApply},
{ViewName: "stash", Key: 'g', Modifier: gocui.ModNone, Handler: gui.handleStashPop},
{ViewName: "stash", Key: 'd', Modifier: gocui.ModNone, Handler: gui.handleStashDrop},
{ViewName: "commitMessage", Key: gocui.KeyEnter, Modifier: gocui.ModNone, Handler: gui.handleCommitConfirm},
{ViewName: "commitMessage", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: gui.handleCommitClose},
{ViewName: "commitMessage", Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.handleNewlineCommitMessage},
}
// Would make these keybindings global but that interferes with editing
// input in the confirmation panel
for _, viewName := range []string{"status", "files", "branches", "commits", "stash"} {
bindings = append(bindings, []Binding{
{ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.nextView},
{ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.previousView},
{ViewName: viewName, Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: gui.nextView},
{ViewName: viewName, Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: gui.cursorUp},
{ViewName: viewName, Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: gui.cursorDown},
{ViewName: viewName, Key: 'h', Modifier: gocui.ModNone, Handler: gui.previousView},
{ViewName: viewName, Key: 'l', Modifier: gocui.ModNone, Handler: gui.nextView},
{ViewName: viewName, Key: 'k', Modifier: gocui.ModNone, Handler: gui.cursorUp},
{ViewName: viewName, Key: 'j', Modifier: gocui.ModNone, Handler: gui.cursorDown},
}...)
}
for _, binding := range bindings {
if err := g.SetKeybinding(binding.ViewName, binding.Key, binding.Modifier, binding.Handler); err != nil {
return err
}
}
if err := gui.setInitialContexts(); err != nil {
return err
}
return nil
}
func (gui *Gui) GetContextMap() map[string]map[string][]*Binding {
return map[string]map[string][]*Binding{
"main": {
"normal": {
{
ViewName: "main",
Key: gocui.MouseWheelDown,
Modifier: gocui.ModNone,
Handler: gui.scrollDownMain,
Description: gui.Tr.SLocalize("ScrollDown"),
Alternative: "fn+up",
}, {
ViewName: "main",
Key: gocui.MouseWheelUp,
Modifier: gocui.ModNone,
Handler: gui.scrollUpMain,
Description: gui.Tr.SLocalize("ScrollUp"),
Alternative: "fn+down",
},
},
"staging": {
{
ViewName: "main",
Key: gocui.KeyEsc,
Modifier: gocui.ModNone,
Handler: gui.handleStagingEscape,
Description: gui.Tr.SLocalize("EscapeStaging"),
}, {
ViewName: "main",
Key: gocui.KeyArrowUp,
Modifier: gocui.ModNone,
Handler: gui.handleStagingPrevLine,
Description: gui.Tr.SLocalize("PrevLine"),
}, {
ViewName: "main",
Key: gocui.KeyArrowDown,
Modifier: gocui.ModNone,
Handler: gui.handleStagingNextLine,
Description: gui.Tr.SLocalize("NextLine"),
}, {
ViewName: "main",
Key: 'k',
Modifier: gocui.ModNone,
Handler: gui.handleStagingPrevLine,
}, {
ViewName: "main",
Key: 'j',
Modifier: gocui.ModNone,
Handler: gui.handleStagingNextLine,
}, {
ViewName: "main",
Key: gocui.MouseWheelUp,
Modifier: gocui.ModNone,
Handler: gui.handleStagingPrevLine,
}, {
ViewName: "main",
Key: gocui.MouseWheelDown,
Modifier: gocui.ModNone,
Handler: gui.handleStagingNextLine,
}, {
ViewName: "main",
Key: gocui.KeyArrowLeft,
Modifier: gocui.ModNone,
Handler: gui.handleStagingPrevHunk,
Description: gui.Tr.SLocalize("PrevHunk"),
}, {
ViewName: "main",
Key: gocui.KeyArrowRight,
Modifier: gocui.ModNone,
Handler: gui.handleStagingNextHunk,
Description: gui.Tr.SLocalize("NextHunk"),
}, {
ViewName: "main",
Key: 'h',
Modifier: gocui.ModNone,
Handler: gui.handleStagingPrevHunk,
}, {
ViewName: "main",
Key: 'l',
Modifier: gocui.ModNone,
Handler: gui.handleStagingNextHunk,
}, {
ViewName: "main",
Key: gocui.KeySpace,
Modifier: gocui.ModNone,
Handler: gui.handleStageLine,
Description: gui.Tr.SLocalize("StageLine"),
}, {
ViewName: "main",
Key: 'a',
Modifier: gocui.ModNone,
Handler: gui.handleStageHunk,
Description: gui.Tr.SLocalize("StageHunk"),
},
},
"merging": {
{
ViewName: "main",
Key: gocui.KeyEsc,
Modifier: gocui.ModNone,
Handler: gui.handleEscapeMerge,
Description: gui.Tr.SLocalize("EscapeStaging"),
}, {
ViewName: "main",
Key: gocui.KeySpace,
Modifier: gocui.ModNone,
Handler: gui.handlePickHunk,
Description: gui.Tr.SLocalize("PickHunk"),
}, {
ViewName: "main",
Key: 'b',
Modifier: gocui.ModNone,
Handler: gui.handlePickBothHunks,
Description: gui.Tr.SLocalize("PickBothHunks"),
}, {
ViewName: "main",
Key: gocui.KeyArrowLeft,
Modifier: gocui.ModNone,
Handler: gui.handleSelectPrevConflict,
Description: gui.Tr.SLocalize("PrevConflict"),
}, {
ViewName: "main",
Key: gocui.KeyArrowRight,
Modifier: gocui.ModNone,
Handler: gui.handleSelectNextConflict,
Description: gui.Tr.SLocalize("NextConflict"),
}, {
ViewName: "main",
Key: gocui.KeyArrowUp,
Modifier: gocui.ModNone,
Handler: gui.handleSelectTop,
Description: gui.Tr.SLocalize("SelectTop"),
}, {
ViewName: "main",
Key: gocui.KeyArrowDown,
Modifier: gocui.ModNone,
Handler: gui.handleSelectBottom,
Description: gui.Tr.SLocalize("SelectBottom"),
}, {
ViewName: "main",
Key: gocui.MouseWheelUp,
Modifier: gocui.ModNone,
Handler: gui.handleSelectTop,
}, {
ViewName: "main",
Key: gocui.MouseWheelDown,
Modifier: gocui.ModNone,
Handler: gui.handleSelectBottom,
}, {
ViewName: "main",
Key: 'h',
Modifier: gocui.ModNone,
Handler: gui.handleSelectPrevConflict,
}, {
ViewName: "main",
Key: 'l',
Modifier: gocui.ModNone,
Handler: gui.handleSelectNextConflict,
}, {
ViewName: "main",
Key: 'k',
Modifier: gocui.ModNone,
Handler: gui.handleSelectTop,
}, {
ViewName: "main",
Key: 'j',
Modifier: gocui.ModNone,
Handler: gui.handleSelectBottom,
}, {
ViewName: "main",
Key: 'z',
Modifier: gocui.ModNone,
Handler: gui.handlePopFileSnapshot,
Description: gui.Tr.SLocalize("Undo"),
},
},
},
}
}
-103
View File
@@ -1,103 +0,0 @@
package gui
import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// list panel functions
func (gui *Gui) handleMenuSelect(g *gocui.Gui, v *gocui.View) error {
return gui.focusPoint(0, gui.State.Panels.Menu.SelectedLine, gui.State.MenuItemCount, v)
}
func (gui *Gui) handleMenuNextLine(g *gocui.Gui, v *gocui.View) error {
panelState := gui.State.Panels.Menu
gui.changeSelectedLine(&panelState.SelectedLine, v.LinesHeight(), false)
return gui.handleMenuSelect(g, v)
}
func (gui *Gui) handleMenuPrevLine(g *gocui.Gui, v *gocui.View) error {
panelState := gui.State.Panels.Menu
gui.changeSelectedLine(&panelState.SelectedLine, v.LinesHeight(), true)
return gui.handleMenuSelect(g, v)
}
// specific functions
func (gui *Gui) renderMenuOptions() error {
optionsMap := map[string]string{
"esc/q": gui.Tr.SLocalize("close"),
"↑ ↓": gui.Tr.SLocalize("navigate"),
"space": gui.Tr.SLocalize("execute"),
}
return gui.renderOptionsMap(optionsMap)
}
func (gui *Gui) handleMenuClose(g *gocui.Gui, v *gocui.View) error {
for _, key := range []gocui.Key{gocui.KeySpace, gocui.KeyEnter} {
if err := g.DeleteKeybinding("menu", key, gocui.ModNone); err != nil {
return err
}
}
err := g.DeleteView("menu")
if err != nil {
return err
}
return gui.returnFocus(g, v)
}
func (gui *Gui) createMenu(title string, items interface{}, itemCount int, handlePress func(int) error) error {
isFocused := gui.g.CurrentView().Name() == "menu"
gui.State.MenuItemCount = itemCount
list, err := utils.RenderList(items, isFocused)
if err != nil {
return err
}
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, false, list)
menuView, _ := gui.g.SetView("menu", x0, y0, x1, y1, 0)
menuView.Title = title
menuView.FgColor = theme.GocuiDefaultTextColor
menuView.Clear()
fmt.Fprint(menuView, list)
gui.State.Panels.Menu.SelectedLine = 0
wrappedHandlePress := func(g *gocui.Gui, v *gocui.View) error {
selectedLine := gui.State.Panels.Menu.SelectedLine
if err := handlePress(selectedLine); err != nil {
return err
}
if _, err := gui.g.View("menu"); err == nil {
if _, err := gui.g.SetViewOnBottom("menu"); err != nil {
return err
}
}
return gui.returnFocus(gui.g, menuView)
}
for _, key := range []gocui.Key{gocui.KeySpace, gocui.KeyEnter} {
_ = gui.g.DeleteKeybinding("menu", key, gocui.ModNone)
if err := gui.g.SetKeybinding("menu", key, gocui.ModNone, wrappedHandlePress); err != nil {
return err
}
}
gui.g.Update(func(g *gocui.Gui) error {
if _, err := gui.g.View("menu"); err == nil {
if _, err := g.SetViewOnTop("menu"); err != nil {
return err
}
}
currentView := gui.g.CurrentView()
return gui.switchFocus(gui.g, currentView, menuView)
})
return nil
}
+69 -100
View File
@@ -11,10 +11,8 @@ import (
"strings"
"github.com/fatih/color"
"github.com/golang-collections/collections/stack"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -22,13 +20,11 @@ func (gui *Gui) findConflicts(content string) ([]commands.Conflict, error) {
conflicts := make([]commands.Conflict, 0)
var newConflict commands.Conflict
for i, line := range utils.SplitLines(content) {
trimmedLine := strings.TrimPrefix(line, "++")
gui.Log.Info(trimmedLine)
if trimmedLine == "<<<<<<< HEAD" || trimmedLine == "<<<<<<< MERGE_HEAD" || trimmedLine == "<<<<<<< Updated upstream" {
if line == "<<<<<<< HEAD" || line == "<<<<<<< MERGE_HEAD" || line == "<<<<<<< Updated upstream" {
newConflict = commands.Conflict{Start: i}
} else if trimmedLine == "=======" {
} else if line == "=======" {
newConflict.Middle = i
} else if strings.HasPrefix(trimmedLine, ">>>>>>> ") {
} else if strings.HasPrefix(line, ">>>>>>> ") {
newConflict.End = i
conflicts = append(conflicts, newConflict)
}
@@ -51,7 +47,7 @@ func (gui *Gui) coloredConflictFile(content string, conflicts []commands.Conflic
conflict, remainingConflicts := gui.shiftConflict(conflicts)
var outputBuffer bytes.Buffer
for i, line := range utils.SplitLines(content) {
colourAttr := theme.DefaultTextColor
colourAttr := color.FgWhite
if i == conflict.Start || i == conflict.Middle || i == conflict.End {
colourAttr = color.FgRed
}
@@ -68,29 +64,29 @@ func (gui *Gui) coloredConflictFile(content string, conflicts []commands.Conflic
}
func (gui *Gui) handleSelectTop(g *gocui.Gui, v *gocui.View) error {
gui.State.Panels.Merging.ConflictTop = true
return gui.refreshMergePanel()
gui.State.ConflictTop = true
return gui.refreshMergePanel(g)
}
func (gui *Gui) handleSelectBottom(g *gocui.Gui, v *gocui.View) error {
gui.State.Panels.Merging.ConflictTop = false
return gui.refreshMergePanel()
gui.State.ConflictTop = false
return gui.refreshMergePanel(g)
}
func (gui *Gui) handleSelectNextConflict(g *gocui.Gui, v *gocui.View) error {
if gui.State.Panels.Merging.ConflictIndex >= len(gui.State.Panels.Merging.Conflicts)-1 {
if gui.State.ConflictIndex >= len(gui.State.Conflicts)-1 {
return nil
}
gui.State.Panels.Merging.ConflictIndex++
return gui.refreshMergePanel()
gui.State.ConflictIndex++
return gui.refreshMergePanel(g)
}
func (gui *Gui) handleSelectPrevConflict(g *gocui.Gui, v *gocui.View) error {
if gui.State.Panels.Merging.ConflictIndex <= 0 {
if gui.State.ConflictIndex <= 0 {
return nil
}
gui.State.Panels.Merging.ConflictIndex--
return gui.refreshMergePanel()
gui.State.ConflictIndex--
return gui.refreshMergePanel(g)
}
func (gui *Gui) isIndexToDelete(i int, conflict commands.Conflict, pick string) bool {
@@ -137,112 +133,105 @@ func (gui *Gui) pushFileSnapshot(g *gocui.Gui) error {
if err != nil {
return err
}
gui.State.Panels.Merging.EditHistory.Push(content)
gui.State.EditHistory.Push(content)
return nil
}
func (gui *Gui) handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error {
if gui.State.Panels.Merging.EditHistory.Len() == 0 {
if gui.State.EditHistory.Len() == 0 {
return nil
}
prevContent := gui.State.Panels.Merging.EditHistory.Pop().(string)
prevContent := gui.State.EditHistory.Pop().(string)
gitFile, err := gui.getSelectedFile(g)
if err != nil {
return err
}
ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644)
return gui.refreshMergePanel()
return gui.refreshMergePanel(g)
}
func (gui *Gui) handlePickHunk(g *gocui.Gui, v *gocui.View) error {
conflict := gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex]
conflict := gui.State.Conflicts[gui.State.ConflictIndex]
gui.pushFileSnapshot(g)
pick := "bottom"
if gui.State.Panels.Merging.ConflictTop {
if gui.State.ConflictTop {
pick = "top"
}
err := gui.resolveConflict(g, conflict, pick)
if err != nil {
panic(err)
}
// if that was the last conflict, finish the merge for this file
if len(gui.State.Panels.Merging.Conflicts) == 1 {
if err := gui.handleCompleteMerge(); err != nil {
return err
}
}
return gui.refreshMergePanel()
gui.refreshMergePanel(g)
return nil
}
func (gui *Gui) handlePickBothHunks(g *gocui.Gui, v *gocui.View) error {
conflict := gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex]
conflict := gui.State.Conflicts[gui.State.ConflictIndex]
gui.pushFileSnapshot(g)
err := gui.resolveConflict(g, conflict, "both")
if err != nil {
panic(err)
}
return gui.refreshMergePanel()
return gui.refreshMergePanel(g)
}
func (gui *Gui) refreshMergePanel() error {
panelState := gui.State.Panels.Merging
cat, err := gui.catSelectedFile(gui.g)
func (gui *Gui) refreshMergePanel(g *gocui.Gui) error {
cat, err := gui.catSelectedFile(g)
if err != nil {
return err
}
if cat == "" {
return nil
}
panelState.Conflicts, err = gui.findConflicts(cat)
gui.State.Conflicts, err = gui.findConflicts(cat)
if err != nil {
return err
}
// handle potential fixes that the user made in their editor since we last refreshed
if len(panelState.Conflicts) == 0 {
return gui.handleCompleteMerge()
} else if panelState.ConflictIndex > len(panelState.Conflicts)-1 {
panelState.ConflictIndex = len(panelState.Conflicts) - 1
if len(gui.State.Conflicts) == 0 {
return gui.handleCompleteMerge(g)
} else if gui.State.ConflictIndex > len(gui.State.Conflicts)-1 {
gui.State.ConflictIndex = len(gui.State.Conflicts) - 1
}
hasFocus := gui.currentViewName() == "main"
content, err := gui.coloredConflictFile(cat, panelState.Conflicts, panelState.ConflictIndex, panelState.ConflictTop, hasFocus)
hasFocus := gui.currentViewName(g) == "main"
if hasFocus {
gui.renderMergeOptions(g)
}
content, err := gui.coloredConflictFile(cat, gui.State.Conflicts, gui.State.ConflictIndex, gui.State.ConflictTop, hasFocus)
if err != nil {
return err
}
if err := gui.renderString(gui.g, "main", content); err != nil {
if err := gui.scrollToConflict(g); err != nil {
return err
}
if err := gui.scrollToConflict(gui.g); err != nil {
return err
}
mainView := gui.getMainView()
mainView.Wrap = false
return nil
return gui.renderString(g, "main", content)
}
func (gui *Gui) scrollToConflict(g *gocui.Gui) error {
panelState := gui.State.Panels.Merging
if len(panelState.Conflicts) == 0 {
mainView, err := g.View("main")
if err != nil {
return err
}
if len(gui.State.Conflicts) == 0 {
return nil
}
mergingView := gui.getMainView()
conflict := panelState.Conflicts[panelState.ConflictIndex]
ox, _ := mergingView.Origin()
_, height := mergingView.Size()
conflict := gui.State.Conflicts[gui.State.ConflictIndex]
ox, _ := mainView.Origin()
_, height := mainView.Size()
conflictMiddle := (conflict.End + conflict.Start) / 2
newOriginY := int(math.Max(0, float64(conflictMiddle-(height/2))))
gui.g.Update(func(g *gocui.Gui) error {
return mergingView.SetOrigin(ox, newOriginY)
})
return nil
return mainView.SetOrigin(ox, newOriginY)
}
func (gui *Gui) renderMergeOptions() error {
return gui.renderOptionsMap(map[string]string{
func (gui *Gui) switchToMerging(g *gocui.Gui) error {
gui.State.ConflictIndex = 0
gui.State.ConflictTop = true
_, err := g.SetCurrentView("main")
if err != nil {
return err
}
return gui.refreshMergePanel(g)
}
func (gui *Gui) renderMergeOptions(g *gocui.Gui) error {
return gui.renderOptionsMap(g, map[string]string{
"↑ ↓": gui.Tr.SLocalize("selectHunk"),
"← →": gui.Tr.SLocalize("navigateConflicts"),
"space": gui.Tr.SLocalize("pickHunk"),
@@ -252,40 +241,20 @@ func (gui *Gui) renderMergeOptions() error {
}
func (gui *Gui) handleEscapeMerge(g *gocui.Gui, v *gocui.View) error {
gui.State.Panels.Merging.EditHistory = stack.New()
if err := gui.refreshFiles(); err != nil {
filesView, err := g.View("files")
if err != nil {
return err
}
// it's possible this method won't be called from the merging view so we need to
// ensure we only 'return' focus if we already have it
if gui.g.CurrentView() == gui.getMainView() {
return gui.switchFocus(g, v, gui.getFilesView())
}
return nil
gui.refreshFiles(g)
return gui.switchFocus(g, v, filesView)
}
func (gui *Gui) handleCompleteMerge() error {
if err := gui.stageSelectedFile(gui.g); err != nil {
func (gui *Gui) handleCompleteMerge(g *gocui.Gui) error {
filesView, err := g.View("files")
if err != nil {
return err
}
if err := gui.refreshFiles(); err != nil {
return err
}
// if we got conflicts after unstashing, we don't want to call any git
// commands to continue rebasing/merging here
if gui.State.WorkingTreeState == "normal" {
return gui.handleEscapeMerge(gui.g, gui.getMainView())
}
// if there are no more files with merge conflicts, we should ask whether the user wants to continue
if !gui.anyFilesWithMergeConflicts() {
return gui.promptToContinue()
}
return gui.handleEscapeMerge(gui.g, gui.getMainView())
}
// promptToContinue asks the user if they want to continue the rebase/merge that's in progress
func (gui *Gui) promptToContinue() error {
return gui.createConfirmationPanel(gui.g, gui.getFilesView(), "continue", gui.Tr.SLocalize("ConflictsResolved"), func(g *gocui.Gui, v *gocui.View) error {
return gui.genericMergeCommand("continue")
}, nil)
gui.stageSelectedFile(g)
gui.refreshFiles(g)
return gui.switchFocus(g, nil, filesView)
}
-53
View File
@@ -1,53 +0,0 @@
package gui
import (
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
)
func (gui *Gui) getBindings(v *gocui.View) []*Binding {
var (
bindingsGlobal, bindingsPanel []*Binding
)
bindings := gui.GetCurrentKeybindings()
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)
handleMenuPress := 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(strings.Title(gui.Tr.SLocalize("menu")), bindings, len(bindings), handleMenuPress)
}
-91
View File
@@ -1,91 +0,0 @@
package gui
import (
"fmt"
"strings"
"github.com/jesseduffield/gocui"
)
type option struct {
value string
}
// GetDisplayStrings is a function.
func (r *option) GetDisplayStrings(isFocused bool) []string {
return []string{r.value}
}
func (gui *Gui) handleCreateRebaseOptionsMenu(g *gocui.Gui, v *gocui.View) error {
options := []*option{
{value: "continue"},
{value: "abort"},
}
if gui.State.WorkingTreeState == "rebasing" {
options = append(options, &option{value: "skip"})
}
handleMenuPress := func(index int) error {
command := options[index].value
return gui.genericMergeCommand(command)
}
var title string
if gui.State.WorkingTreeState == "merging" {
title = gui.Tr.SLocalize("MergeOptionsTitle")
} else {
title = gui.Tr.SLocalize("RebaseOptionsTitle")
}
return gui.createMenu(title, options, len(options), handleMenuPress)
}
func (gui *Gui) genericMergeCommand(command string) error {
status := gui.State.WorkingTreeState
if status != "merging" && status != "rebasing" {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NotMergingOrRebasing"))
}
commandType := strings.Replace(status, "ing", "e", 1)
// we should end up with a command like 'git merge --continue'
// it's impossible for a rebase to require a commit so we'll use a subprocess only if it's a merge
if status == "merging" && command != "abort" && gui.Config.GetUserConfig().GetBool("git.merging.manualCommit") {
sub := gui.OSCommand.PrepareSubProcess("git", commandType, fmt.Sprintf("--%s", command))
if sub != nil {
gui.SubProcess = sub
return gui.Errors.ErrSubProcess
}
return nil
}
result := gui.GitCommand.GenericMerge(commandType, command)
if err := gui.handleGenericMergeCommandResult(result); err != nil {
return err
}
return nil
}
func (gui *Gui) handleGenericMergeCommandResult(result error) error {
if err := gui.refreshSidePanels(gui.g); err != nil {
return err
}
if result == nil {
return nil
} else if result == gui.Errors.ErrSubProcess {
return result
} else if strings.Contains(result.Error(), "No changes - did you forget to use") {
return gui.genericMergeCommand("skip")
} else if strings.Contains(result.Error(), "When you have resolved this problem") || strings.Contains(result.Error(), "fix conflicts") || strings.Contains(result.Error(), "Resolve all conflicts manually") {
return gui.createConfirmationPanel(gui.g, gui.getFilesView(), gui.Tr.SLocalize("FoundConflictsTitle"), gui.Tr.SLocalize("FoundConflicts"),
func(g *gocui.Gui, v *gocui.View) error {
return nil
}, func(g *gocui.Gui, v *gocui.View) error {
return gui.genericMergeCommand("abort")
},
)
} else {
return gui.createErrorPanel(gui.g, result.Error())
}
}
-76
View File
@@ -1,76 +0,0 @@
package gui
import (
"os"
"path/filepath"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type recentRepo struct {
path string
}
// GetDisplayStrings returns the path from a recent repo.
func (r *recentRepo) GetDisplayStrings(isFocused bool) []string {
yellow := color.New(color.FgMagenta)
base := filepath.Base(r.path)
path := yellow.Sprint(r.path)
return []string{base, path}
}
func (gui *Gui) handleCreateRecentReposMenu(g *gocui.Gui, v *gocui.View) error {
recentRepoPaths := gui.Config.GetAppState().RecentRepos
reposCount := utils.Min(len(recentRepoPaths), 20)
// we won't show the current repo hence the -1
recentRepos := make([]*recentRepo, reposCount-1)
for i, path := range recentRepoPaths[1:reposCount] {
recentRepos[i] = &recentRepo{path: path}
}
handleMenuPress := func(index int) error {
repo := recentRepos[index]
if err := os.Chdir(repo.path); err != nil {
return err
}
newGitCommand, err := commands.NewGitCommand(gui.Log, gui.OSCommand, gui.Tr, gui.Config)
if err != nil {
return err
}
gui.GitCommand = newGitCommand
return gui.Errors.ErrSwitchRepo
}
return gui.createMenu(gui.Tr.SLocalize("RecentRepos"), recentRepos, len(recentRepos), handleMenuPress)
}
// updateRecentRepoList registers the fact that we opened lazygit in this repo,
// so that we can open the same repo via the 'recent repos' menu
func (gui *Gui) updateRecentRepoList() error {
recentRepos := gui.Config.GetAppState().RecentRepos
currentRepo, err := os.Getwd()
if err != nil {
return err
}
known, recentRepos := newRecentReposList(recentRepos, currentRepo)
gui.Config.SetIsNewRepo(known)
gui.Config.GetAppState().RecentRepos = recentRepos
return gui.Config.SaveAppState()
}
// newRecentReposList returns a new repo list with a new entry but only when it doesn't exist yet
func newRecentReposList(recentRepos []string, currentRepo string) (bool, []string) {
isNew := true
newRepos := []string{currentRepo}
for _, repo := range recentRepos {
if repo != currentRepo {
newRepos = append(newRepos, repo)
} else {
isNew = false
}
}
return isNew, newRepos
}
-222
View File
@@ -1,222 +0,0 @@
package gui
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/git"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) refreshStagingPanel() error {
file, err := gui.getSelectedFile(gui.g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
}
return gui.handleStagingEscape(gui.g, nil)
}
if !file.HasUnstagedChanges {
return gui.handleStagingEscape(gui.g, nil)
}
// note for custom diffs, we'll need to send a flag here saying not to use the custom diff
diff := gui.GitCommand.Diff(file, true)
colorDiff := gui.GitCommand.Diff(file, false)
if len(diff) < 2 {
return gui.handleStagingEscape(gui.g, nil)
}
// parse the diff and store the line numbers of hunks and stageable lines
// TODO: maybe instantiate this at application start
p, err := git.NewPatchParser(gui.Log)
if err != nil {
return nil
}
hunkStarts, stageableLines, err := p.ParsePatch(diff)
if err != nil {
return nil
}
var selectedLine int
if gui.State.Panels.Staging != nil {
end := len(stageableLines) - 1
if end < gui.State.Panels.Staging.SelectedLine {
selectedLine = end
} else {
selectedLine = gui.State.Panels.Staging.SelectedLine
}
} else {
selectedLine = 0
}
gui.State.Panels.Staging = &stagingPanelState{
StageableLines: stageableLines,
HunkStarts: hunkStarts,
SelectedLine: selectedLine,
Diff: diff,
}
if len(stageableLines) == 0 {
return gui.createErrorPanel(gui.g, "No lines to stage")
}
if err := gui.focusLineAndHunk(); err != nil {
return err
}
mainView := gui.getMainView()
mainView.Highlight = true
mainView.Wrap = false
gui.g.Update(func(*gocui.Gui) error {
return gui.setViewContent(gui.g, gui.getMainView(), colorDiff)
})
return nil
}
func (gui *Gui) handleStagingEscape(g *gocui.Gui, v *gocui.View) error {
gui.State.Panels.Staging = nil
return gui.switchFocus(gui.g, nil, gui.getFilesView())
}
func (gui *Gui) handleStagingPrevLine(g *gocui.Gui, v *gocui.View) error {
return gui.handleCycleLine(true)
}
func (gui *Gui) handleStagingNextLine(g *gocui.Gui, v *gocui.View) error {
return gui.handleCycleLine(false)
}
func (gui *Gui) handleStagingPrevHunk(g *gocui.Gui, v *gocui.View) error {
return gui.handleCycleHunk(true)
}
func (gui *Gui) handleStagingNextHunk(g *gocui.Gui, v *gocui.View) error {
return gui.handleCycleHunk(false)
}
func (gui *Gui) handleCycleHunk(prev bool) error {
state := gui.State.Panels.Staging
lineNumbers := state.StageableLines
currentLine := lineNumbers[state.SelectedLine]
currentHunkIndex := utils.PrevIndex(state.HunkStarts, currentLine)
var newHunkIndex int
if prev {
if currentHunkIndex == 0 {
newHunkIndex = len(state.HunkStarts) - 1
} else {
newHunkIndex = currentHunkIndex - 1
}
} else {
if currentHunkIndex == len(state.HunkStarts)-1 {
newHunkIndex = 0
} else {
newHunkIndex = currentHunkIndex + 1
}
}
state.SelectedLine = utils.NextIndex(lineNumbers, state.HunkStarts[newHunkIndex])
return gui.focusLineAndHunk()
}
func (gui *Gui) handleCycleLine(prev bool) error {
state := gui.State.Panels.Staging
lineNumbers := state.StageableLines
currentLine := lineNumbers[state.SelectedLine]
var newIndex int
if prev {
newIndex = utils.PrevIndex(lineNumbers, currentLine)
} else {
newIndex = utils.NextIndex(lineNumbers, currentLine)
}
state.SelectedLine = newIndex
return gui.focusLineAndHunk()
}
// focusLineAndHunk works out the best focus for the staging panel given the
// selected line and size of the hunk
func (gui *Gui) focusLineAndHunk() error {
stagingView := gui.getMainView()
state := gui.State.Panels.Staging
lineNumber := state.StageableLines[state.SelectedLine]
// we want the bottom line of the view buffer to ideally be the bottom line
// of the hunk, but if the hunk is too big we'll just go three lines beyond
// the currently selected line so that the user can see the context
var bottomLine int
nextHunkStartIndex := utils.NextIndex(state.HunkStarts, lineNumber)
if nextHunkStartIndex == 0 {
// for now linesHeight is an efficient means of getting the number of lines
// in the patch. However if we introduce word wrap we'll need to update this
bottomLine = stagingView.LinesHeight() - 1
} else {
bottomLine = state.HunkStarts[nextHunkStartIndex] - 1
}
hunkStartIndex := utils.PrevIndex(state.HunkStarts, lineNumber)
hunkStart := state.HunkStarts[hunkStartIndex]
// if it's the first hunk we'll also show the diff header
if hunkStartIndex == 0 {
hunkStart = 0
}
_, height := stagingView.Size()
// if this hunk is too big, we will just ensure that the user can at least
// see three lines of context below the cursor
if bottomLine-hunkStart > height {
bottomLine = lineNumber + 3
}
return gui.generalFocusLine(lineNumber, bottomLine, stagingView)
}
func (gui *Gui) handleStageHunk(g *gocui.Gui, v *gocui.View) error {
return gui.handleStageLineOrHunk(true)
}
func (gui *Gui) handleStageLine(g *gocui.Gui, v *gocui.View) error {
return gui.handleStageLineOrHunk(false)
}
func (gui *Gui) handleStageLineOrHunk(hunk bool) error {
state := gui.State.Panels.Staging
p, err := git.NewPatchModifier(gui.Log)
if err != nil {
return err
}
currentLine := state.StageableLines[state.SelectedLine]
var patch string
if hunk {
patch, err = p.ModifyPatchForHunk(state.Diff, state.HunkStarts, currentLine)
} else {
patch, err = p.ModifyPatchForLine(state.Diff, currentLine)
}
if err != nil {
return err
}
// for logging purposes
// ioutil.WriteFile("patch.diff", []byte(patch), 0600)
// apply the patch then refresh this panel
// create a new temp file with the patch, then call git apply with that patch
_, err = gui.GitCommand.ApplyPatch(patch)
if err != nil {
return err
}
if err := gui.refreshFiles(); err != nil {
return err
}
if err := gui.refreshStagingPanel(); err != nil {
return err
}
return nil
}
+39 -78
View File
@@ -5,96 +5,56 @@ import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// list panel functions
func (gui *Gui) getSelectedStashEntry(v *gocui.View) *commands.StashEntry {
selectedLine := gui.State.Panels.Stash.SelectedLine
if selectedLine == -1 {
return nil
}
return gui.State.StashEntries[selectedLine]
}
func (gui *Gui) handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
stashEntry := gui.getSelectedStashEntry(v)
if stashEntry == nil {
return gui.renderString(g, "main", gui.Tr.SLocalize("NoStashEntries"))
}
if err := gui.focusPoint(0, gui.State.Panels.Stash.SelectedLine, len(gui.State.StashEntries), v); err != nil {
return err
}
go func() {
// doing this asynchronously cos it can take time
diff, _ := gui.GitCommand.GetStashEntryDiff(stashEntry.Index)
_ = gui.renderString(g, "main", diff)
}()
return nil
}
func (gui *Gui) refreshStashEntries(g *gocui.Gui) error {
g.Update(func(g *gocui.Gui) error {
gui.State.StashEntries = gui.GitCommand.GetStashEntries()
gui.refreshSelectedLine(&gui.State.Panels.Stash.SelectedLine, len(gui.State.StashEntries))
isFocused := gui.g.CurrentView().Name() == "stash"
list, err := utils.RenderList(gui.State.StashEntries, isFocused)
v, err := g.View("stash")
if err != nil {
return err
panic(err)
}
v := gui.getStashView()
gui.State.StashEntries = gui.GitCommand.GetStashEntries()
v.Clear()
fmt.Fprint(v, list)
if err := gui.resetOrigin(v); err != nil {
return err
for _, stashEntry := range gui.State.StashEntries {
fmt.Fprintln(v, stashEntry.DisplayString)
}
return nil
return gui.resetOrigin(v)
})
return nil
}
func (gui *Gui) handleStashNextLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
func (gui *Gui) getSelectedStashEntry(v *gocui.View) *commands.StashEntry {
if len(gui.State.StashEntries) == 0 {
return nil
}
panelState := gui.State.Panels.Stash
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.StashEntries), false)
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
return gui.handleStashEntrySelect(gui.g, v)
lineNumber := gui.getItemPosition(v)
return &gui.State.StashEntries[lineNumber]
}
func (gui *Gui) handleStashPrevLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
panelState := gui.State.Panels.Stash
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.State.StashEntries), true)
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
return gui.handleStashEntrySelect(gui.g, v)
func (gui *Gui) renderStashOptions(g *gocui.Gui) error {
return gui.renderOptionsMap(g, map[string]string{
"space": gui.Tr.SLocalize("apply"),
"g": gui.Tr.SLocalize("pop"),
"d": gui.Tr.SLocalize("drop"),
"← → ↑ ↓": gui.Tr.SLocalize("navigate"),
})
}
// specific functions
func (gui *Gui) handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error {
if err := gui.renderStashOptions(g); err != nil {
return err
}
go func() {
stashEntry := gui.getSelectedStashEntry(v)
if stashEntry == nil {
gui.renderString(g, "main", gui.Tr.SLocalize("NoStashEntries"))
return
}
diff, _ := gui.GitCommand.GetStashEntryDiff(stashEntry.Index)
gui.renderString(g, "main", diff)
}()
return nil
}
func (gui *Gui) handleStashApply(g *gocui.Gui, v *gocui.View) error {
return gui.stashDo(g, v, "apply")
@@ -127,18 +87,19 @@ func (gui *Gui) stashDo(g *gocui.Gui, v *gocui.View, method string) error {
gui.createErrorPanel(g, err.Error())
}
gui.refreshStashEntries(g)
return gui.refreshFiles()
return gui.refreshFiles(g)
}
func (gui *Gui) handleStashSave(stashFunc func(message string) error) error {
func (gui *Gui) handleStashSave(g *gocui.Gui, filesView *gocui.View) error {
if len(gui.trackedFiles()) == 0 && len(gui.stagedFiles()) == 0 {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NoTrackedStagedFilesStash"))
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoTrackedStagedFilesStash"))
}
return gui.createPromptPanel(gui.g, gui.getFilesView(), gui.Tr.SLocalize("StashChanges"), func(g *gocui.Gui, v *gocui.View) error {
if err := stashFunc(gui.trimmedContent(v)); err != nil {
gui.createPromptPanel(g, filesView, gui.Tr.SLocalize("StashChanges"), func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.StashSave(gui.trimmedContent(v)); err != nil {
gui.createErrorPanel(g, err.Error())
}
gui.refreshStashEntries(g)
return gui.refreshFiles()
return gui.refreshFiles(g)
})
return nil
}
+22 -49
View File
@@ -2,7 +2,6 @@ package gui
import (
"fmt"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
@@ -19,14 +18,14 @@ func (gui *Gui) refreshStatus(g *gocui.Gui) error {
// contents end up cleared
g.Update(func(*gocui.Gui) error {
v.Clear()
pushables, pullables := gui.GitCommand.GetCurrentBranchUpstreamDifferenceCount()
pushables, pullables := gui.GitCommand.UpstreamDifferenceCount()
fmt.Fprint(v, "↑"+pushables+"↓"+pullables)
branches := gui.State.Branches
if err := gui.updateWorkTreeState(); err != nil {
if err := gui.updateHasMergeConflictStatus(); err != nil {
return err
}
if gui.State.WorkingTreeState != "normal" {
fmt.Fprint(v, utils.ColoredString(fmt.Sprintf(" (%s)", gui.State.WorkingTreeState), color.FgYellow))
if gui.State.HasMergeConflicts {
fmt.Fprint(v, utils.ColoredString(" (merging)", color.FgYellow))
}
if len(branches) == 0 {
@@ -42,42 +41,37 @@ func (gui *Gui) refreshStatus(g *gocui.Gui) error {
return nil
}
func (gui *Gui) handleCheckForUpdate(g *gocui.Gui, v *gocui.View) error {
gui.Updater.CheckForNewUpdate(gui.onUserUpdateCheckFinish, true)
return gui.createLoaderPanel(gui.g, v, gui.Tr.SLocalize("CheckingForUpdates"))
func (gui *Gui) renderStatusOptions(g *gocui.Gui) error {
return gui.renderOptionsMap(g, map[string]string{
"o": gui.Tr.SLocalize("OpenConfig"),
"e": gui.Tr.SLocalize("EditConfig"),
})
}
func (gui *Gui) handleStatusSelect(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
dashboardString := fmt.Sprintf(
"%s\n\n%s\n\n%s\n\n%s\n\n%s",
lazygitTitle(),
"Keybindings: https://github.com/jesseduffield/lazygit/blob/master/docs/Keybindings.md",
"Config Options: https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md",
"Tutorial: https://www.youtube.com/watch?v=VDXvbHZYeKY",
"Raise an Issue: https://github.com/jesseduffield/lazygit/issues",
)
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
if err := gui.renderString(g, "main", dashboardString); err != nil {
return err
}
magenta := color.New(color.FgMagenta)
dashboardString := strings.Join(
[]string{
lazygitTitle(),
"Copyright (c) 2018 Jesse Duffield",
"Keybindings: https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings",
"Config Options: https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md",
"Tutorial: https://youtu.be/VDXvbHZYeKY",
"Raise an Issue: https://github.com/jesseduffield/lazygit/issues",
magenta.Sprint("Buy Jesse a coffee: https://donorbox.org/lazygit"), // caffeine ain't free
}, "\n\n")
return gui.renderString(g, "main", dashboardString)
return gui.renderStatusOptions(g)
}
func (gui *Gui) handleOpenConfig(g *gocui.Gui, v *gocui.View) error {
return gui.openFile(gui.Config.GetUserConfig().ConfigFileUsed())
filename := gui.Config.GetUserConfig().ConfigFileUsed()
return gui.genericFileOpen(g, v, filename, gui.OSCommand.OpenFile)
}
func (gui *Gui) handleEditConfig(g *gocui.Gui, v *gocui.View) error {
filename := gui.Config.GetUserConfig().ConfigFileUsed()
return gui.editFile(filename)
return gui.genericFileOpen(g, v, filename, gui.OSCommand.EditFile)
}
func lazygitTitle() string {
@@ -91,24 +85,3 @@ func lazygitTitle() string {
__/ | __/ |
|___/ |___/ `
}
func (gui *Gui) updateWorkTreeState() error {
merging, err := gui.GitCommand.IsInMergeState()
if err != nil {
return err
}
if merging {
gui.State.WorkingTreeState = "merging"
return nil
}
rebaseMode, err := gui.GitCommand.RebaseMode()
if err != nil {
return err
}
if rebaseMode != "" {
gui.State.WorkingTreeState = "rebasing"
return nil
}
gui.State.WorkingTreeState = "normal"
return nil
}
+54
View File
@@ -0,0 +1,54 @@
package gui
import (
"github.com/jesseduffield/gocui"
)
// GetAttribute gets the gocui color attribute from the string
func (gui *Gui) GetAttribute(key string) gocui.Attribute {
colorMap := map[string]gocui.Attribute{
"default": gocui.ColorDefault,
"black": gocui.ColorBlack,
"red": gocui.ColorRed,
"green": gocui.ColorGreen,
"yellow": gocui.ColorYellow,
"blue": gocui.ColorBlue,
"magenta": gocui.ColorMagenta,
"cyan": gocui.ColorCyan,
"white": gocui.ColorWhite,
"bold": gocui.AttrBold,
"reverse": gocui.AttrReverse,
"underline": gocui.AttrUnderline,
}
value, present := colorMap[key]
if present {
return value
}
return gocui.ColorWhite
}
// GetColor bitwise OR's a list of attributes obtained via the given keys
func (gui *Gui) GetColor(keys []string) gocui.Attribute {
var attribute gocui.Attribute
for _, key := range keys {
attribute = attribute | gui.GetAttribute(key)
}
return attribute
}
// GetOptionsPanelTextColor gets the color of the options panel text
func (gui *Gui) GetOptionsPanelTextColor() (gocui.Attribute, error) {
userConfig := gui.Config.GetUserConfig()
optionsColor := userConfig.GetStringSlice("gui.theme.optionsTextColor")
return gui.GetColor(optionsColor), nil
}
// SetColorScheme sets the color scheme for the app based on the user config
func (gui *Gui) SetColorScheme() error {
userConfig := gui.Config.GetUserConfig()
activeBorderColor := userConfig.GetStringSlice("gui.theme.activeBorderColor")
inactiveBorderColor := userConfig.GetStringSlice("gui.theme.inactiveBorderColor")
gui.g.FgColor = gui.GetColor(inactiveBorderColor)
gui.g.SelFgColor = gui.GetColor(activeBorderColor)
return nil
}
-65
View File
@@ -1,65 +0,0 @@
package gui
import "github.com/jesseduffield/gocui"
func (gui *Gui) showUpdatePrompt(newVersion string) error {
title := "New version available!"
message := "Download latest version? (enter/esc)"
currentView := gui.g.CurrentView()
return gui.createConfirmationPanel(gui.g, currentView, title, message, func(g *gocui.Gui, v *gocui.View) error {
gui.startUpdating(newVersion)
return nil
}, nil)
}
func (gui *Gui) onUserUpdateCheckFinish(newVersion string, err error) error {
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
if newVersion == "" {
return gui.createErrorPanel(gui.g, "New version not found")
}
return gui.showUpdatePrompt(newVersion)
}
func (gui *Gui) onBackgroundUpdateCheckFinish(newVersion string, err error) error {
if err != nil {
// ignoring the error for now so that I'm not annoying users
gui.Log.Error(err.Error())
return nil
}
if newVersion == "" {
return nil
}
if gui.Config.GetUserConfig().Get("update.method") == "background" {
gui.startUpdating(newVersion)
return nil
}
return gui.showUpdatePrompt(newVersion)
}
func (gui *Gui) startUpdating(newVersion string) {
gui.State.Updating = true
gui.statusManager.addWaitingStatus("updating")
gui.Updater.Update(newVersion, gui.onUpdateFinish)
}
func (gui *Gui) onUpdateFinish(err error) error {
gui.State.Updating = false
gui.statusManager.removeStatus("updating")
if err := gui.renderString(gui.g, "appStatus", ""); err != nil {
return err
}
if err != nil {
return gui.createErrorPanel(gui.g, "Update failed: "+err.Error())
}
return nil
}
func (gui *Gui) createUpdateQuitConfirmation(g *gocui.Gui, v *gocui.View) error {
title := "Currently Updating"
message := "An update is in progress. Are you sure you want to quit?"
return gui.createConfirmationPanel(gui.g, v, title, message, func(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}, nil)
}
+113 -264
View File
@@ -4,26 +4,18 @@ import (
"fmt"
"sort"
"strings"
"time"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/spkg/bom"
)
var cyclableViews = []string{"status", "files", "branches", "commits", "stash"}
func (gui *Gui) refreshSidePanels(g *gocui.Gui) error {
if err := gui.refreshBranches(g); err != nil {
return err
}
if err := gui.refreshFiles(); err != nil {
return err
}
if err := gui.refreshCommits(g); err != nil {
return err
}
return gui.refreshStashEntries(g)
gui.refreshBranches(g)
gui.refreshFiles(g)
gui.refreshCommits(g)
return nil
}
func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error {
@@ -31,13 +23,8 @@ func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error {
if v == nil || v.Name() == cyclableViews[len(cyclableViews)-1] {
focusedViewName = cyclableViews[0]
} else {
// if we're in the commitFiles view we'll act like we're in the commits view
viewName := v.Name()
if viewName == "commitFiles" {
viewName = "commits"
}
for i := range cyclableViews {
if viewName == cyclableViews[i] {
if v.Name() == cyclableViews[i] {
focusedViewName = cyclableViews[i+1]
break
}
@@ -45,7 +32,7 @@ func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error {
message := gui.Tr.TemplateLocalize(
"IssntListOfViews",
Teml{
"name": viewName,
"name": v.Name(),
},
)
gui.Log.Info(message)
@@ -65,13 +52,8 @@ func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error {
if v == nil || v.Name() == cyclableViews[0] {
focusedViewName = cyclableViews[len(cyclableViews)-1]
} else {
// if we're in the commitFiles view we'll act like we're in the commits view
viewName := v.Name()
if viewName == "commitFiles" {
viewName = "commits"
}
for i := range cyclableViews {
if viewName == cyclableViews[i] {
if v.Name() == cyclableViews[i] {
focusedViewName = cyclableViews[i-1] // TODO: make this work properly
break
}
@@ -79,7 +61,7 @@ func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error {
message := gui.Tr.TemplateLocalize(
"IssntListOfViews",
Teml{
"name": viewName,
"name": v.Name(),
},
)
gui.Log.Info(message)
@@ -95,33 +77,29 @@ func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error {
mainView, _ := g.View("main")
mainView.SetOrigin(0, 0)
switch v.Name() {
case "menu":
return gui.handleMenuSelect(g, v)
case "status":
return gui.handleStatusSelect(g, v)
case "files":
return gui.handleFileSelect(g, v, false)
return gui.handleFileSelect(g, v)
case "branches":
return gui.handleBranchSelect(g, v)
case "commits":
return gui.handleCommitSelect(g, v)
case "commitFiles":
return gui.handleCommitFileSelect(g, v)
case "stash":
return gui.handleStashEntrySelect(g, v)
case "confirmation":
return nil
case "commitMessage":
return gui.handleCommitFocused(g, v)
case "credentials":
return gui.handleCredentialsViewFocused(g, v)
case "main":
if gui.State.Contexts["main"] == "merging" {
return gui.refreshMergePanel()
}
// TODO: pull this out into a 'view focused' function
gui.refreshMergePanel(g)
v.Highlight = false
return nil
case "commits":
return gui.handleCommitSelect(g, v)
case "stash":
return gui.handleStashEntrySelect(g, v)
default:
panic(gui.Tr.SLocalize("NoViewMachingNewLineFocusedSwitchStatement"))
}
@@ -130,51 +108,27 @@ func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) returnFocus(g *gocui.Gui, v *gocui.View) error {
previousView, err := g.View(gui.State.PreviousView)
if err != nil {
// always fall back to files view if there's no 'previous' view stored
previousView, err = g.View("files")
if err != nil {
gui.Log.Error(err)
}
panic(err)
}
return gui.switchFocus(g, v, previousView)
}
func (gui *Gui) goToSideView(sideViewName string) func(g *gocui.Gui, v *gocui.View) error {
return func(g *gocui.Gui, v *gocui.View) error {
view, err := g.View(sideViewName)
if err != nil {
gui.Log.Error(err)
return nil
}
err = gui.closePopupPanels()
if err != nil {
gui.Log.Error(err)
return nil
}
return gui.switchFocus(g, nil, view)
}
}
func (gui *Gui) closePopupPanels() error {
gui.onNewPopupPanel()
err := gui.closeConfirmationPrompt(gui.g)
if err != nil {
gui.Log.Error(err)
return err
}
return nil
}
// pass in oldView = nil if you don't want to be able to return to your old view
// TODO: move some of this logic into our onFocusLost and onFocus hooks
func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error {
// we assume we'll never want to return focus to a popup panel i.e.
// we should never stack popup panels
if oldView != nil && !gui.isPopupPanel(oldView.Name()) {
// we assume we'll never want to return focus to a confirmation panel i.e.
// we should never stack confirmation panels
if oldView != nil && oldView.Name() != "confirmation" {
oldView.Highlight = false
message := gui.Tr.TemplateLocalize(
"settingPreviewsViewTo",
Teml{
"oldViewName": oldView.Name(),
},
)
gui.Log.Info(message)
gui.State.PreviousView = oldView.Name()
}
gui.Log.Info("setting highlight to true for view" + newView.Name())
newView.Highlight = true
message := gui.Tr.TemplateLocalize(
"newFocusedViewIs",
Teml{
@@ -185,77 +139,87 @@ func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error {
if _, err := g.SetCurrentView(newView.Name()); err != nil {
return err
}
if _, err := g.SetViewOnTop(newView.Name()); err != nil {
return err
}
g.Cursor = newView.Editable
if err := gui.renderPanelOptions(); err != nil {
return err
}
return gui.newLineFocused(g, newView)
}
func (gui *Gui) getItemPosition(v *gocui.View) int {
gui.correctCursor(v)
_, cy := v.Cursor()
_, oy := v.Origin()
return oy + cy
}
func (gui *Gui) cursorUp(g *gocui.Gui, v *gocui.View) error {
// swallowing cursor movements in main
// TODO: pull this out
if v == nil || v.Name() == "main" {
return nil
}
ox, oy := v.Origin()
cx, cy := v.Cursor()
if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 {
if err := v.SetOrigin(ox, oy-1); err != nil {
return err
}
}
gui.newLineFocused(g, v)
return nil
}
func (gui *Gui) cursorDown(g *gocui.Gui, v *gocui.View) error {
// swallowing cursor movements in main
// TODO: pull this out
if v == nil || v.Name() == "main" {
return nil
}
cx, cy := v.Cursor()
ox, oy := v.Origin()
if cy+oy >= len(v.BufferLines())-2 {
return nil
}
if err := v.SetCursor(cx, cy+1); err != nil {
if err := v.SetOrigin(ox, oy+1); err != nil {
return err
}
}
gui.newLineFocused(g, v)
return nil
}
func (gui *Gui) resetOrigin(v *gocui.View) error {
_ = v.SetCursor(0, 0)
if err := v.SetCursor(0, 0); err != nil {
return err
}
return v.SetOrigin(0, 0)
}
// if the cursor down past the last item, move it to the last line
func (gui *Gui) focusPoint(cx int, cy int, lineCount int, v *gocui.View) error {
if cy < 0 || cy > lineCount {
return nil
}
ox, oy := v.Origin()
_, height := v.Size()
ly := height - 1
if ly == -1 {
ly = 0
}
// if line is above origin, move origin and set cursor to zero
// if line is below origin + height, move origin and set cursor to max
// otherwise set cursor to value - origin
if ly > lineCount {
_ = v.SetCursor(cx, cy)
_ = v.SetOrigin(ox, 0)
} else if cy < oy {
_ = v.SetCursor(cx, 0)
_ = v.SetOrigin(ox, cy)
} else if cy > oy+ly {
_ = v.SetCursor(cx, ly)
_ = v.SetOrigin(ox, cy-ly)
} else {
_ = v.SetCursor(cx, cy-oy)
func (gui *Gui) correctCursor(v *gocui.View) error {
cx, cy := v.Cursor()
_, oy := v.Origin()
lineCount := len(v.BufferLines()) - 2
if cy >= lineCount-oy {
return v.SetCursor(cx, lineCount-oy)
}
return nil
}
func (gui *Gui) cleanString(s string) string {
output := string(bom.Clean([]byte(s)))
return utils.NormalizeLinefeeds(output)
}
func (gui *Gui) setViewContent(g *gocui.Gui, v *gocui.View, s string) error {
v.Clear()
fmt.Fprint(v, gui.cleanString(s))
return nil
}
// renderString resets the origin of a view and sets its content
func (gui *Gui) renderString(g *gocui.Gui, viewName, s string) error {
g.Update(func(*gocui.Gui) error {
v, err := g.View(viewName)
// just in case the view disappeared as this function was called, we'll
// silently return if it's not found
if err != nil {
return nil // return gracefully if view has been deleted
return nil
}
if err := v.SetOrigin(0, 0); err != nil {
return err
}
return gui.setViewContent(gui.g, v, s)
v.Clear()
fmt.Fprint(v, s)
v.Wrap = true
return nil
})
return nil
}
@@ -269,44 +233,31 @@ func (gui *Gui) optionsMapToString(optionsMap map[string]string) string {
return strings.Join(optionsArray, ", ")
}
func (gui *Gui) renderOptionsMap(optionsMap map[string]string) error {
return gui.renderString(gui.g, "options", gui.optionsMapToString(optionsMap))
func (gui *Gui) renderOptionsMap(g *gocui.Gui, optionsMap map[string]string) error {
return gui.renderString(g, "options", gui.optionsMapToString(optionsMap))
}
func (gui *Gui) loader() string {
characters := "|/-\\"
now := time.Now()
nanos := now.UnixNano()
index := nanos / 50000000 % int64(len(characters))
return characters[index : index+1]
}
// TODO: refactor properly
// i'm so sorry but had to add this getBranchesView
func (gui *Gui) getFilesView() *gocui.View {
v, _ := gui.g.View("files")
func (gui *Gui) getFilesView(g *gocui.Gui) *gocui.View {
v, _ := g.View("files")
return v
}
func (gui *Gui) getCommitsView() *gocui.View {
v, _ := gui.g.View("commits")
func (gui *Gui) getCommitsView(g *gocui.Gui) *gocui.View {
v, _ := g.View("commits")
return v
}
func (gui *Gui) getCommitMessageView() *gocui.View {
v, _ := gui.g.View("commitMessage")
return v
}
func (gui *Gui) getBranchesView() *gocui.View {
v, _ := gui.g.View("branches")
return v
}
func (gui *Gui) getMainView() *gocui.View {
v, _ := gui.g.View("main")
return v
}
func (gui *Gui) getStashView() *gocui.View {
v, _ := gui.g.View("stash")
return v
}
func (gui *Gui) getCommitFilesView() *gocui.View {
v, _ := gui.g.View("commitFiles")
func (gui *Gui) getCommitMessageView(g *gocui.Gui) *gocui.View {
v, _ := g.View("commitMessage")
return v
}
@@ -314,109 +265,7 @@ func (gui *Gui) trimmedContent(v *gocui.View) string {
return strings.TrimSpace(v.Buffer())
}
func (gui *Gui) currentViewName() string {
currentView := gui.g.CurrentView()
func (gui *Gui) currentViewName(g *gocui.Gui) string {
currentView := g.CurrentView()
return currentView.Name()
}
func (gui *Gui) resizeCurrentPopupPanel(g *gocui.Gui) error {
v := g.CurrentView()
if gui.isPopupPanel(v.Name()) {
return gui.resizePopupPanel(g, v)
}
return nil
}
func (gui *Gui) resizePopupPanel(g *gocui.Gui, v *gocui.View) error {
// If the confirmation panel is already displayed, just resize the width,
// otherwise continue
content := v.Buffer()
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, v.Wrap, content)
vx0, vy0, vx1, vy1 := v.Dimensions()
if vx0 == x0 && vy0 == y0 && vx1 == x1 && vy1 == y1 {
return nil
}
gui.Log.Info(gui.Tr.SLocalize("resizingPopupPanel"))
_, err := g.SetView(v.Name(), x0, y0, x1, y1, 0)
return err
}
// generalFocusLine takes a lineNumber to focus, and a bottomLine to ensure we can see
func (gui *Gui) generalFocusLine(lineNumber int, bottomLine int, v *gocui.View) error {
_, height := v.Size()
overScroll := bottomLine - height + 1
if overScroll < 0 {
overScroll = 0
}
if err := v.SetOrigin(0, overScroll); err != nil {
return err
}
if err := v.SetCursor(0, lineNumber-overScroll); err != nil {
return err
}
return nil
}
func (gui *Gui) changeSelectedLine(line *int, total int, up bool) {
if up {
if *line == -1 || *line == 0 {
return
}
*line -= 1
} else {
if *line == -1 || *line == total-1 {
return
}
*line += 1
}
}
func (gui *Gui) refreshSelectedLine(line *int, total int) {
if *line == -1 && total > 0 {
*line = 0
} else if total-1 < *line {
*line = total - 1
}
}
func (gui *Gui) renderListPanel(v *gocui.View, items interface{}) error {
gui.g.Update(func(g *gocui.Gui) error {
isFocused := gui.g.CurrentView().Name() == v.Name()
list, err := utils.RenderList(items, isFocused)
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
v.Clear()
fmt.Fprint(v, list)
return nil
})
return nil
}
func (gui *Gui) renderPanelOptions() error {
currentView := gui.g.CurrentView()
switch currentView.Name() {
case "menu":
return gui.renderMenuOptions()
case "main":
if gui.State.Contexts["main"] == "merging" {
return gui.renderMergeOptions()
}
}
return gui.renderGlobalOptions()
}
func (gui *Gui) handleFocusView(g *gocui.Gui, v *gocui.View) error {
_, err := gui.g.SetCurrentView(v.Name())
return err
}
func (gui *Gui) isPopupPanel(viewName string) bool {
return viewName == "commitMessage" || viewName == "credentials" || viewName == "confirmation" || viewName == "menu"
}
func (gui *Gui) popupPanelFocused() bool {
return gui.isPopupPanel(gui.currentViewName())
}
+39 -507
View File
@@ -6,19 +6,16 @@ import (
)
// addDutch will add all dutch translations
func addDutch(i18nObject *i18n.Bundle) error {
func addDutch(i18nObject *i18n.Bundle) {
// add the translations
return i18nObject.AddMessages(language.Dutch,
i18nObject.AddMessages(language.Dutch,
&i18n.Message{
ID: "NotEnoughSpace",
Other: "Niet genoeg ruimte om de panelen te renderen",
}, &i18n.Message{
ID: "DiffTitle",
Other: "Diff",
}, &i18n.Message{
ID: "LogTitle",
Other: "Log",
}, &i18n.Message{
ID: "FilesTitle",
Other: "Bestanden",
@@ -28,63 +25,24 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "CommitsTitle",
Other: "Commits",
}, &i18n.Message{
ID: "CommitsDiffTitle",
Other: "Commits (specific diff mode)",
}, &i18n.Message{
ID: "CommitsDiff",
Other: "select commit to diff with another commit",
}, &i18n.Message{
ID: "StashTitle",
Other: "Stash",
}, &i18n.Message{
ID: "StagingMainTitle",
Other: `Stage Lines/Hunks`,
}, &i18n.Message{
ID: "MergingMainTitle",
Other: "Resolve merge conflicts",
}, &i18n.Message{
ID: "CommitMessage",
Other: "Commit bericht",
}, &i18n.Message{
ID: "CredentialsUsername",
Other: "Gebruikersnaam",
}, &i18n.Message{
ID: "CredentialsPassword",
Other: "Wachtwoord",
}, &i18n.Message{
ID: "PassUnameWrong",
Other: "Wachtwoord en/of gebruikersnaam verkeert",
Other: "Commit Bericht",
}, &i18n.Message{
ID: "CommitChanges",
Other: "Commit veranderingen",
}, &i18n.Message{
ID: "AmendLastCommit",
Other: "wijzig laatste commit",
}, &i18n.Message{
ID: "SureToAmend",
Other: "Weet je zeker dat je de laatste commit wilt wijzigen? U kunt het commit-bericht wijzigen vanuit het commits-paneel.",
}, &i18n.Message{
ID: "NoCommitToAmend",
Other: "Er is geen commits om te wijzigen.",
}, &i18n.Message{
ID: "CommitChangesWithEditor",
Other: "commit veranderingen met de git editor",
Other: "Commit Veranderingen",
}, &i18n.Message{
ID: "StatusTitle",
Other: "Status",
}, &i18n.Message{
ID: "GlobalTitle",
Other: "Global",
}, &i18n.Message{
ID: "navigate",
Other: "navigeer",
}, &i18n.Message{
ID: "menu",
Other: "menu",
}, &i18n.Message{
ID: "execute",
Other: "uitvoeren",
ID: "stashFiles",
Other: "stash-bestanden",
}, &i18n.Message{
ID: "open",
Other: "open",
@@ -97,24 +55,15 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "toggleStaged",
Other: "toggle staged",
}, &i18n.Message{
ID: "toggleStagedAll",
Other: "toggle staged alle",
}, &i18n.Message{
ID: "refresh",
Other: "verversen",
}, &i18n.Message{
ID: "push",
Other: "push",
}, &i18n.Message{
ID: "pull",
Other: "pull",
}, &i18n.Message{
ID: "addPatch",
Other: "bewerkingen toevoegen",
Other: "verandering toevoegen",
}, &i18n.Message{
ID: "edit",
Other: "bewerken",
Other: "veranderen",
}, &i18n.Message{
ID: "scroll",
Other: "scroll",
@@ -123,13 +72,13 @@ func addDutch(i18nObject *i18n.Bundle) error {
Other: "samenvoegen afbreken",
}, &i18n.Message{
ID: "resolveMergeConflicts",
Other: "los merge conflicten op",
Other: "verhelp samenvoegen fouten",
}, &i18n.Message{
ID: "checkout",
Other: "uitchecken",
}, &i18n.Message{
ID: "NoChangedFiles",
Other: "Geen bestanden veranderd",
Other: "Geen Bestanden verandert",
}, &i18n.Message{
ID: "FileHasNoUnstagedChanges",
Other: "Het bestand heeft geen unstaged veranderingen om toe te voegen",
@@ -145,30 +94,27 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "NoFilesDisplay",
Other: "Geen bestanden om te laten zien",
}, &i18n.Message{
ID: "NotAFile",
Other: "Dit is geen bestand",
}, &i18n.Message{
ID: "PullWait",
Other: "Pullen...",
Other: "Pulling...",
}, &i18n.Message{
ID: "PushWait",
Other: "Pushen...",
}, &i18n.Message{
ID: "FetchWait",
Other: "Fetchen...",
Other: "Pushing...",
}, &i18n.Message{
ID: "FileNoMergeCons",
Other: "Dit bestand heeft geen merge conflicten",
}, &i18n.Message{
ID: "SureResetHardHead",
Other: "Weet je het zeker dat je `reset --hard HEAD` wil uitvoeren? het kan dat je hierdoor bestanden verliest",
}, &i18n.Message{
ID: "SureTo",
Other: "Weet je het zeker dat je {{.fileName}} wilt {{.deleteVerb}} (je veranderingen zullen worden verwijderd)",
Other: "Weet je het zeker dat je {{.fileName}} wilt {{.deleteVerb}} (je veranderingen zullen worden verwijdert)",
}, &i18n.Message{
ID: "AlreadyCheckedOutBranch",
Other: "Je hebt deze branch al uitgecheckt",
Other: "Je hebt uitgecheckt op deze branch",
}, &i18n.Message{
ID: "SureForceCheckout",
Other: "Weet je zeker dat je het uitchecken wil forceren? Al je lokale verandering zullen worden verwijdert",
Other: "Weet je zeker dat je het uitchecken wil forceren? al je locale verandering zullen worden verwijdert",
}, &i18n.Message{
ID: "ForceCheckoutBranch",
Other: "Forceer uitchecken op deze branch",
@@ -186,13 +132,7 @@ func addDutch(i18nObject *i18n.Bundle) error {
Other: "Verwijder branch",
}, &i18n.Message{
ID: "DeleteBranchMessage",
Other: "Weet je zeker dat je branch {{.selectedBranchName}} wilt verwijderen?",
}, &i18n.Message{
ID: "ForceDeleteBranchMessage",
Other: "Weet je zeker dat je branch {{.selectedBranchName}} geforceerd wil verwijderen?",
}, &i18n.Message{
ID: "rebaseBranch",
Other: "rebase branch",
Other: "Weet je zeker dat je {{.selectedBranchName}} branch wil verwijderen?",
}, &i18n.Message{
ID: "CantMergeBranchIntoItself",
Other: "Je kan niet een branch in zichzelf mergen",
@@ -201,7 +141,7 @@ func addDutch(i18nObject *i18n.Bundle) error {
Other: "forceer checkout",
}, &i18n.Message{
ID: "merge",
Other: "samenvoegen",
Other: "merge",
}, &i18n.Message{
ID: "checkoutByName",
Other: "uitchecken bij naam",
@@ -211,9 +151,6 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "deleteBranch",
Other: "verwijder branch",
}, &i18n.Message{
ID: "forceDeleteBranch",
Other: "verwijder branch (forceer)",
}, &i18n.Message{
ID: "NoBranchesThisRepo",
Other: "Geen branches voor deze repo",
@@ -226,9 +163,6 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "CloseConfirm",
Other: "{{.keyBindClose}}: Sluiten, {{.keyBindConfirm}}: Bevestigen",
}, &i18n.Message{
ID: "close",
Other: "sluiten",
}, &i18n.Message{
ID: "SureResetThisCommit",
Other: "Weet je het zeker dat je wil resetten naar deze commit?",
@@ -240,7 +174,7 @@ func addDutch(i18nObject *i18n.Bundle) error {
Other: "squash beneden",
}, &i18n.Message{
ID: "rename",
Other: "hernoemen",
Other: "hernoem",
}, &i18n.Message{
ID: "resetToThisCommit",
Other: "reset naar deze commit",
@@ -269,17 +203,17 @@ func addDutch(i18nObject *i18n.Bundle) error {
ID: "OnlyRenameTopCommit",
Other: "Je kan alleen de bovenste commit hernoemen",
}, &i18n.Message{
ID: "renameCommit",
Other: "hernoem commit",
}, &i18n.Message{
ID: "renameCommitEditor",
Other: "rename commit with editor",
ID: "RenameCommit",
Other: "Hernoem Commit",
}, &i18n.Message{
ID: "PotentialErrInGetselectedCommit",
Other: "Er is mogelijk een error in getSelected Commit (geen match tussen ui en state)",
}, &i18n.Message{
ID: "NoCommitsThisBranch",
Other: "Geen commits voor deze branch",
}, &i18n.Message{
ID: "Error",
Other: "Foutmelding",
Other: "Fout",
}, &i18n.Message{
ID: "resizingPopupPanel",
Other: "resizen popup paneel",
@@ -288,16 +222,16 @@ func addDutch(i18nObject *i18n.Bundle) error {
Other: "subprocess lopend",
}, &i18n.Message{
ID: "selectHunk",
Other: "selecteer stuk",
Other: "selecteer Hunk",
}, &i18n.Message{
ID: "navigateConflicts",
Other: "navigeer conflicts",
}, &i18n.Message{
ID: "pickHunk",
Other: "kies stuk",
Other: "kies Hunk",
}, &i18n.Message{
ID: "pickBothHunks",
Other: "kies beide stukken",
Other: "kies bijde hunks",
}, &i18n.Message{
ID: "undo",
Other: "ongedaan maken",
@@ -334,426 +268,24 @@ func addDutch(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "NoViewMachingNewLineFocusedSwitchStatement",
Other: "Er machen geen weergave met de newLineFocused switch declaratie",
}, &i18n.Message{
ID: "settingPreviewsViewTo",
Other: "vorige weergave instellen op: {{.oldViewName}}",
}, &i18n.Message{
ID: "newFocusedViewIs",
Other: "nieuw gefocussed weergave is {{.newFocusedView}}",
}, &i18n.Message{
ID: "CantCloseConfirmationPrompt",
Other: "Kon de bevestiging prompt niet sluiten: {{.error}}",
}, &i18n.Message{
ID: "NoChangedFiles",
Other: "Geen veranderde files",
}, &i18n.Message{
ID: "ClearFilePanel",
Other: "maak bestandsvenster leeg",
}, &i18n.Message{
ID: "MergeAborted",
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: "Jouw branch is afgeweken van de remote branch. Druk 'esc' om te annuleren, of 'enter' om geforceert te pushen.",
}, &i18n.Message{
ID: "checkForUpdate",
Other: "check voor updates",
}, &i18n.Message{
ID: "CheckingForUpdates",
Other: "zoeken naar updates...",
}, &i18n.Message{
ID: "OnLatestVersionErr",
Other: "Je hebt al de laatste versie",
}, &i18n.Message{
ID: "MajorVersionErr",
Other: "Nieuwe versie ({{.newVersion}}) is niet backwards compatibele vergeleken met de huidige versie ({{.currentVersion}})",
}, &i18n.Message{
ID: "CouldNotFindBinaryErr",
Other: "Kon geen binary vinden op {{.url}}",
}, &i18n.Message{
ID: "AnonymousReportingTitle",
Other: "Help lazygit te verbeteren",
}, &i18n.Message{
ID: "AnonymousReportingPrompt",
Other: "Zou je anonieme data rapportage willen aanzetten om lazygit beter te kunnen maken? (enter/esc)",
}, &i18n.Message{
ID: "GitconfigParseErr",
Other: `Gogit kon je gitconfig bestand niet goed parsen door de aanwezigheid van losstaande '\' tekens. Het weghalen van deze tekens zou het probleem moeten oplossen. `,
}, &i18n.Message{
ID: "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: "mergeIntoCurrentBranch",
Other: `merge in met huidige checked out branch`,
}, &i18n.Message{
ID: "ConfirmQuit",
Other: `Weet je zeker dat je dit programma wil sluiten?`,
}, &i18n.Message{
ID: "SwitchRepo",
Other: "wissel naar een recente repo",
}, &i18n.Message{
ID: "UnsupportedGitService",
Other: `Niet-ondersteunde git-service`,
}, &i18n.Message{
ID: "createPullRequest",
Other: `maak een pull-aanvraag`,
}, &i18n.Message{
ID: "NoBranchOnRemote",
Other: `Deze branch bestaat niet op de remote. U moet het eerst naar de remote pushen.`,
}, &i18n.Message{
ID: "fetch",
Other: `fetch`,
}, &i18n.Message{
ID: "NoAutomaticGitFetchTitle",
Other: `Geen automatische git fetch`,
}, &i18n.Message{
ID: "NoAutomaticGitFetchBody",
Other: `Lazygit kan niet "git fetch" uitvoeren in een privé repository, gebruik f in het branches paneel om "git fetch" manueel uit te voeren`,
}, &i18n.Message{
ID: "StageLines",
Other: `stage individuele hunks/lijnen`,
}, &i18n.Message{
ID: "FileStagingRequirements",
Other: `Kan alleen individuele lijnen stagen van getrackte bestanden met onstaged veranderingen`,
}, &i18n.Message{
ID: "StagingTitle",
Other: `Stage Lines/Hunks`,
}, &i18n.Message{
ID: "StageHunk",
Other: `stage hunk`,
}, &i18n.Message{
ID: "StageLine",
Other: `stage lijn`,
}, &i18n.Message{
ID: "EscapeStaging",
Other: `ga terug naar het bestanden paneel`,
}, &i18n.Message{
ID: "CantFindHunks",
Other: `Kan geen hunks vinden in deze patch`,
}, &i18n.Message{
ID: "CantFindHunk",
Other: `Kan geen hunk vinden`,
}, &i18n.Message{
ID: "RebasingTitle",
Other: "Rebasing",
}, &i18n.Message{
ID: "MergingTitle",
Other: "Merging",
}, &i18n.Message{
ID: "ConfirmRebase",
Other: "Weet je zeker dat je {{.checkedOutBranch}} op {{.selectedBranch}} wil rebasen?",
}, &i18n.Message{
ID: "ConfirmMerge",
Other: "Weet je zeker dat je {{.selectedBranch}} in {{.checkedOutBranch}} wil mergen?",
}, &i18n.Message{
ID: "FwdNoUpstream",
Other: "Kan niet de branch vooruitspoelen zonder upstream",
}, &i18n.Message{
ID: "ErrorOccurred",
Other: "Er is iets fout gegaan! Zou je hier een issue aan willen maken: https://github.com/jesseduffield/lazygit/issues",
}, &i18n.Message{
ID: "FwdCommitsToPush",
Other: "Je kan niet vooruitspoelen als de branch geen nieuwe commits heeft",
}, &i18n.Message{
ID: "MainTitle",
Other: "Hoofd",
}, &i18n.Message{
ID: "NormalTitle",
Other: "Normaal",
}, &i18n.Message{
ID: "softReset",
Other: "zacht reset",
}, &i18n.Message{
ID: "CantRebaseOntoSelf",
Other: "Je kan niet een branch rebasen op zichzelf",
}, &i18n.Message{
ID: "SureSquashThisCommit",
Other: "Weet je zeker dat je deze commit wil samenvoegen met de commit hieronder?",
}, &i18n.Message{
ID: "Squash",
Other: "Squash",
}, &i18n.Message{
ID: "pickCommit",
Other: "pick commit (when mid-rebase)",
}, &i18n.Message{
ID: "revertCommit",
Other: "commit omgedaan maken",
}, &i18n.Message{
ID: "deleteCommit",
Other: "verwijder commit",
}, &i18n.Message{
ID: "moveDownCommit",
Other: "verplaats commit 1 omlaag",
}, &i18n.Message{
ID: "moveUpCommit",
Other: "verplaats commit 1 omhoog",
}, &i18n.Message{
ID: "editCommit",
Other: "verander commit",
}, &i18n.Message{
ID: "amendToCommit",
Other: "wijzig commit met staged veranderingen",
}, &i18n.Message{
ID: "FoundConflicts",
Other: "Conflicten!, Om af te breken druk 'esc', anders druk op 'enter'",
}, &i18n.Message{
ID: "FoundConflictsTitle",
Other: "Auto-merge mislukt",
}, &i18n.Message{
ID: "Undo",
Other: "ongedaan maken",
}, &i18n.Message{
ID: "PickHunk",
Other: "pick hunk",
}, &i18n.Message{
ID: "PickBothHunks",
Other: "pick beide hunks",
}, &i18n.Message{
ID: "ViewMergeRebaseOptions",
Other: "bekijk merge/rebase opties",
}, &i18n.Message{
ID: "NotMergingOrRebasing",
Other: "Je bent momenteel niet aan het rebasen of mergen",
}, &i18n.Message{
ID: "RecentRepos",
Other: "recent repositories",
}, &i18n.Message{
ID: "MergeOptionsTitle",
Other: "Merge Opties",
}, &i18n.Message{
ID: "RebaseOptionsTitle",
Other: "Rebase Opties",
}, &i18n.Message{
ID: "ConflictsResolved",
Other: "alle merge conflicten zijn opgelost. Wilt je verder gaan?",
}, &i18n.Message{
ID: "NoRoom",
Other: "Niet genoeg ruimte",
}, &i18n.Message{
ID: "YouAreHere",
Other: "JE BENT HIER",
}, &i18n.Message{
ID: "rewordNotSupported",
Other: "herformatteren van commits in interactief rebasen is nog niet ondersteund",
}, &i18n.Message{
ID: "cherryPickCopy",
Other: "kopiëer commit (cherry-pick)",
}, &i18n.Message{
ID: "cherryPickCopyRange",
Other: "kopiëer commit reeks (cherry-pick)",
}, &i18n.Message{
ID: "pasteCommits",
Other: "plak commits (cherry-pick)",
}, &i18n.Message{
ID: "SureCherryPick",
Other: "Weet je zeker dat je de gekopieerde commits naar deze branch wil cherry-picken?",
}, &i18n.Message{
ID: "CherryPick",
Other: "Cherry-Pick",
}, &i18n.Message{
ID: "CannotRebaseOntoFirstCommit",
Other: "Je kan niet interactief rebasen naar de eerste commit",
}, &i18n.Message{
ID: "CannotSquashOntoSecondCommit",
Other: "Je kan niet een squash/fixup doen naar de 2de commit",
}, &i18n.Message{
ID: "Donate",
Other: "Doneer",
}, &i18n.Message{
ID: "PrevLine",
Other: "selecteer de vorige lijn",
}, &i18n.Message{
ID: "NextLine",
Other: "selecteer de volgende lijn",
}, &i18n.Message{
ID: "PrevHunk",
Other: "selecteer de vorige hunk",
}, &i18n.Message{
ID: "NextHunk",
Other: "selecteer de volgende hunk",
}, &i18n.Message{
ID: "PrevConflict",
Other: "selecteer voorgaand conflict",
}, &i18n.Message{
ID: "NextConflict",
Other: "selecteer volgende conflict",
}, &i18n.Message{
ID: "SelectTop",
Other: "selecteer bovenste hunk",
}, &i18n.Message{
ID: "SelectBottom",
Other: "selecteer onderste hunk",
}, &i18n.Message{
ID: "ScrollDown",
Other: "scroll omlaag",
}, &i18n.Message{
ID: "ScrollUp",
Other: "scroll omhoog",
}, &i18n.Message{
ID: "AmendCommitTitle",
Other: "Commit wijzigen",
}, &i18n.Message{
ID: "AmendCommitPrompt",
Other: "Weet je zeker dat je deze commit wil wijzigen met de vorige staged bestanden?",
}, &i18n.Message{
ID: "DeleteCommitTitle",
Other: "Verwijder Commit",
}, &i18n.Message{
ID: "DeleteCommitPrompt",
Other: "Weet je zeker dat je deze commit wil verwijderen?",
}, &i18n.Message{
ID: "SquashingStatus",
Other: "squashing",
}, &i18n.Message{
ID: "FixingStatus",
Other: "fixing up",
}, &i18n.Message{
ID: "DeletingStatus",
Other: "verwijderen",
}, &i18n.Message{
ID: "MovingStatus",
Other: "verplaatsen",
}, &i18n.Message{
ID: "RebasingStatus",
Other: "rebasing",
}, &i18n.Message{
ID: "AmendingStatus",
Other: "wijzigen",
}, &i18n.Message{
ID: "CherryPickingStatus",
Other: "cherry-picking",
}, &i18n.Message{
ID: "CommitFiles",
Other: "Commit bestanden",
}, &i18n.Message{
ID: "viewCommitFiles",
Other: "bekijk gecommite bestanden",
}, &i18n.Message{
ID: "CommitFilesTitle",
Other: "Commit bestanden",
}, &i18n.Message{
ID: "goBack",
Other: "ga terug",
}, &i18n.Message{
ID: "NoCommiteFiles",
Other: "Geen bestanden voor deze commit",
}, &i18n.Message{
ID: "checkoutCommitFile",
Other: "bestand uitchecken",
}, &i18n.Message{
ID: "discardOldFileChange",
Other: "uitsluit deze commit zijn veranderingen aan dit bestand",
}, &i18n.Message{
ID: "DiscardFileChangesTitle",
Other: "uitsluit bestand zijn veranderingen",
}, &i18n.Message{
ID: "DiscardFileChangesPrompt",
Other: "Weet je zeker dat je de wijzigingen van deze commit in dit bestand wilt weggooien? Als dit bestand is gecreëerd in deze commit dan zal dit bestand worden verwijdert",
}, &i18n.Message{
ID: "DisabledForGPG",
Other: "Onderdelen niet beschikbaar voor gebruikers die GPG gebruiken",
}, &i18n.Message{
ID: "CreateRepo",
Other: "Niet in een git repository. Creëer een nieuwe git repository? (y/n): ",
}, &i18n.Message{
ID: "AutoStashTitle",
Other: "Autostash?",
}, &i18n.Message{
ID: "AutoStashPrompt",
Other: "Je moet je veranderingen stashen en poppen om ze over te bregen. Dit automatisch doen? (enter/esc)",
}, &i18n.Message{
ID: "StashPrefix",
Other: "Auto-stashing veranderingen voor ",
}, &i18n.Message{
ID: "viewDiscardOptions",
Other: "bekijk 'veranderingen ongedaan maken' opties",
}, &i18n.Message{
ID: "cancel",
Other: "anuleren",
}, &i18n.Message{
ID: "discardAllChanges",
Other: "negeer alle wijzigingen",
}, &i18n.Message{
ID: "discardUnstagedChanges",
Other: "negeer unstaged wijzigingen",
}, &i18n.Message{
ID: "discardAllChangesToAllFiles",
Other: "verwijder werkende tree",
}, &i18n.Message{
ID: "discardAnyUnstagedChanges",
Other: "discard unstaged changes",
}, &i18n.Message{
ID: "discardUntrackedFiles",
Other: "negeer niet-gevonden bestanden",
}, &i18n.Message{
ID: "viewResetOptions",
Other: `bekijk reset opties`,
}, &i18n.Message{
ID: "hardReset",
Other: "harde reset",
}, &i18n.Message{
ID: "createFixupCommit",
Other: `creëer fixup commit voor deze commit`,
}, &i18n.Message{
ID: "squashAboveCommits",
Other: `squash bovenstaande commits`,
}, &i18n.Message{
ID: "SquashAboveCommits",
Other: `Squash bovenstaande commits`,
}, &i18n.Message{
ID: "SureSquashAboveCommits",
Other: `Weet je zeker dat je alles wil squash/fixup! voor de bovenstaand commits {{.commit}}?`,
}, &i18n.Message{
ID: "CreateFixupCommit",
Other: `Creëer fixup commit`,
}, &i18n.Message{
ID: "SureCreateFixupCommit",
Other: `Weet je zeker dat je een fixup wil maken! commit voor commit {{.commit}}?`,
}, &i18n.Message{
ID: "executeCustomCommand",
Other: "voor aangepast commando uit",
}, &i18n.Message{
ID: "CustomCommand",
Other: "Aangepast commando:",
}, &i18n.Message{
ID: "commitChangesWithoutHook",
Other: "commit veranderingen zonder pre-commit hook",
}, &i18n.Message{
ID: "SkipHookPrefixNotConfigured",
Other: "Je hebt nog niet een commit bericht voorvoegsel ingesteld voor het overslaan van hooks. Set `git.skipHookPrefix = 'WIP'` in je config",
}, &i18n.Message{
ID: "resetTo",
Other: `reset to`,
}, &i18n.Message{
ID: "pressEnterToReturn",
Other: "Press enter to return to lazygit",
}, &i18n.Message{
ID: "viewStashOptions",
Other: "view stash options",
}, &i18n.Message{
ID: "stashAllChanges",
Other: "stash-bestanden",
}, &i18n.Message{
ID: "stashStagedChanges",
Other: "stash staged changes",
}, &i18n.Message{
ID: "stashOptions",
Other: "Stash options",
}, &i18n.Message{
ID: "notARepository",
Other: "Error: must be run inside a git repository",
}, &i18n.Message{
ID: "jump",
Other: "jump to panel",
},
)
}
+18 -495
View File
@@ -15,18 +15,15 @@ import (
"golang.org/x/text/language"
)
func addEnglish(i18nObject *i18n.Bundle) error {
func addEnglish(i18nObject *i18n.Bundle) {
return i18nObject.AddMessages(language.English,
i18nObject.AddMessages(language.English,
&i18n.Message{
ID: "NotEnoughSpace",
Other: "Not enough space to render panels",
}, &i18n.Message{
ID: "DiffTitle",
Other: "Diff",
}, &i18n.Message{
ID: "LogTitle",
Other: "Log",
}, &i18n.Message{
ID: "FilesTitle",
Other: "Files",
@@ -36,75 +33,24 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "CommitsTitle",
Other: "Commits",
}, &i18n.Message{
ID: "CommitsDiffTitle",
Other: "Commits (specific diff mode)",
}, &i18n.Message{
ID: "CommitsDiff",
Other: "select commit to diff with another commit",
}, &i18n.Message{
ID: "StashTitle",
Other: "Stash",
}, &i18n.Message{
ID: "StagingMainTitle",
Other: `Stage Lines/Hunks`,
}, &i18n.Message{
ID: "MergingMainTitle",
Other: "Resolve merge conflicts",
}, &i18n.Message{
ID: "MainTitle",
Other: "Main",
}, &i18n.Message{
ID: "StagingTitle",
Other: "Staging",
}, &i18n.Message{
ID: "MergingTitle",
Other: "Merging",
}, &i18n.Message{
ID: "NormalTitle",
Other: "Normal",
}, &i18n.Message{
ID: "CommitMessage",
Other: "Commit message",
}, &i18n.Message{
ID: "CredentialsUsername",
Other: "Username",
}, &i18n.Message{
ID: "CredentialsPassword",
Other: "Password",
}, &i18n.Message{
ID: "PassUnameWrong",
Other: "Password and/or username wrong",
}, &i18n.Message{
ID: "CommitChanges",
Other: "commit changes",
}, &i18n.Message{
ID: "AmendLastCommit",
Other: "amend last commit",
}, &i18n.Message{
ID: "SureToAmend",
Other: "Are you sure you want to amend last commit? Afterwards, you can change commit message from the commits panel.",
}, &i18n.Message{
ID: "NoCommitToAmend",
Other: "There's no commit to amend.",
}, &i18n.Message{
ID: "CommitChangesWithEditor",
Other: "commit changes using git editor",
}, &i18n.Message{
ID: "StatusTitle",
Other: "Status",
}, &i18n.Message{
ID: "GlobalTitle",
Other: "Global",
}, &i18n.Message{
ID: "navigate",
Other: "navigate",
}, &i18n.Message{
ID: "menu",
Other: "menu",
}, &i18n.Message{
ID: "execute",
Other: "execute",
ID: "stashFiles",
Other: "stash files",
}, &i18n.Message{
ID: "open",
Other: "open",
@@ -117,18 +63,9 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "toggleStaged",
Other: "toggle staged",
}, &i18n.Message{
ID: "toggleStagedAll",
Other: "stage/unstage all",
}, &i18n.Message{
ID: "refresh",
Other: "refresh",
}, &i18n.Message{
ID: "push",
Other: "push",
}, &i18n.Message{
ID: "pull",
Other: "pull",
}, &i18n.Message{
ID: "addPatch",
Other: "add patch",
@@ -165,24 +102,18 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "NoFilesDisplay",
Other: "No file to display",
}, &i18n.Message{
ID: "NotAFile",
Other: "Not a file",
}, &i18n.Message{
ID: "PullWait",
Other: "Pulling...",
}, &i18n.Message{
ID: "PushWait",
Other: "Pushing...",
}, &i18n.Message{
ID: "FetchWait",
Other: "Fetching...",
}, &i18n.Message{
ID: "FileNoMergeCons",
Other: "This file has no inline merge conflicts",
Other: "This file has no merge conflicts",
}, &i18n.Message{
ID: "softReset",
Other: "soft reset",
ID: "SureResetHardHead",
Other: "Are you sure you want `reset --hard HEAD`? You may lose changes",
}, &i18n.Message{
ID: "SureTo",
Other: "Are you sure you want to {{.deleteVerb}} {{.fileName}} (you will lose your changes)?",
@@ -209,16 +140,7 @@ func addEnglish(i18nObject *i18n.Bundle) error {
Other: "Delete Branch",
}, &i18n.Message{
ID: "DeleteBranchMessage",
Other: "Are you sure you want to delete the branch {{.selectedBranchName}}?",
}, &i18n.Message{
ID: "ForceDeleteBranchMessage",
Other: "{{.selectedBranchName}} is not fully merged. Are you sure you want to delete it?",
}, &i18n.Message{
ID: "rebaseBranch",
Other: "rebase branch",
}, &i18n.Message{
ID: "CantRebaseOntoSelf",
Other: "You cannot rebase a branch onto itself",
Other: "Are you sure you want delete the branch {{.selectedBranchName}} ?",
}, &i18n.Message{
ID: "CantMergeBranchIntoItself",
Other: "You cannot merge a branch into itself",
@@ -237,9 +159,6 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "deleteBranch",
Other: "delete branch",
}, &i18n.Message{
ID: "forceDeleteBranch",
Other: "delete branch (force)",
}, &i18n.Message{
ID: "NoBranchesThisRepo",
Other: "No branches for this repo",
@@ -252,9 +171,6 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "CloseConfirm",
Other: "{{.keyBindClose}}: close, {{.keyBindConfirm}}: confirm",
}, &i18n.Message{
ID: "close",
Other: "close",
}, &i18n.Message{
ID: "SureResetThisCommit",
Other: "Are you sure you want to reset to this commit?",
@@ -290,43 +206,13 @@ func addEnglish(i18nObject *i18n.Bundle) error {
Other: "Fixup",
}, &i18n.Message{
ID: "SureFixupThisCommit",
Other: "Are you sure you want to 'fixup' this commit? It will be merged into the commit below",
}, &i18n.Message{
ID: "SureSquashThisCommit",
Other: "Are you sure you want to squash this commit into the commit below?",
}, &i18n.Message{
ID: "Squash",
Other: "Squash",
}, &i18n.Message{
ID: "pickCommit",
Other: "pick commit (when mid-rebase)",
}, &i18n.Message{
ID: "revertCommit",
Other: "revert commit",
Other: "Are you sure you want to fixup this commit? The commit beneath will be squashed up into this one",
}, &i18n.Message{
ID: "OnlyRenameTopCommit",
Other: "Can only reword topmost commit from within lazygit. Use shift+R instead",
Other: "Can only rename topmost commit",
}, &i18n.Message{
ID: "renameCommit",
Other: "reword commit",
}, &i18n.Message{
ID: "deleteCommit",
Other: "delete commit",
}, &i18n.Message{
ID: "moveDownCommit",
Other: "move commit down one",
}, &i18n.Message{
ID: "moveUpCommit",
Other: "move commit up one",
}, &i18n.Message{
ID: "editCommit",
Other: "edit commit",
}, &i18n.Message{
ID: "amendToCommit",
Other: "amend commit with staged changes",
}, &i18n.Message{
ID: "renameCommitEditor",
Other: "rename commit with editor",
ID: "RenameCommit",
Other: "Rename Commit",
}, &i18n.Message{
ID: "PotentialErrInGetselectedCommit",
Other: "potential error in getSelected Commit (mismatched ui and state)",
@@ -390,6 +276,9 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "NoViewMachingNewLineFocusedSwitchStatement",
Other: "No view matching newLineFocused switch statement",
}, &i18n.Message{
ID: "settingPreviewsViewTo",
Other: "setting previous view to: {{.oldViewName}}",
}, &i18n.Message{
ID: "newFocusedViewIs",
Other: "new focused view is {{.newFocusedView}}",
@@ -399,6 +288,9 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "NoChangedFiles",
Other: "No changed files",
}, &i18n.Message{
ID: "ClearFilePanel",
Other: "Clear file panel",
}, &i18n.Message{
ID: "MergeAborted",
Other: "Merge aborted",
@@ -408,375 +300,6 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "EditConfig",
Other: "edit config file",
}, &i18n.Message{
ID: "ForcePush",
Other: "Force push",
}, &i18n.Message{
ID: "ForcePushPrompt",
Other: "Your branch has diverged from the remote branch. Press 'esc' to cancel, or 'enter' to force push.",
}, &i18n.Message{
ID: "checkForUpdate",
Other: "check for update",
}, &i18n.Message{
ID: "CheckingForUpdates",
Other: "Checking for updates...",
}, &i18n.Message{
ID: "OnLatestVersionErr",
Other: "You already have the latest version",
}, &i18n.Message{
ID: "MajorVersionErr",
Other: "New version ({{.newVersion}}) has non-backwards compatible changes compared to the current version ({{.currentVersion}})",
}, &i18n.Message{
ID: "CouldNotFindBinaryErr",
Other: "Could not find any binary at {{.url}}",
}, &i18n.Message{
ID: "AnonymousReportingTitle",
Other: "Help make lazygit better",
}, &i18n.Message{
ID: "AnonymousReportingPrompt",
Other: "Would you like to enable anonymous reporting data to help improve lazygit? (enter/esc)",
}, &i18n.Message{
ID: "GitconfigParseErr",
Other: `Gogit failed to parse your gitconfig file due to the presence of unquoted '\' characters. Removing these should fix the issue.`,
}, &i18n.Message{
ID: "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: "mergeIntoCurrentBranch",
Other: `merge into currently checked out branch`,
}, &i18n.Message{
ID: "ConfirmQuit",
Other: `Are you sure you want to quit?`,
}, &i18n.Message{
ID: "SwitchRepo",
Other: `switch to a recent repo`,
}, &i18n.Message{
ID: "UnsupportedGitService",
Other: `Unsupported git service`,
}, &i18n.Message{
ID: "createPullRequest",
Other: `create pull request`,
}, &i18n.Message{
ID: "NoBranchOnRemote",
Other: `This branch doesn't exist on remote. You need to push it to remote first.`,
}, &i18n.Message{
ID: "fetch",
Other: `fetch`,
}, &i18n.Message{
ID: "NoAutomaticGitFetchTitle",
Other: `No automatic git fetch`,
}, &i18n.Message{
ID: "NoAutomaticGitFetchBody",
Other: `Lazygit can't use "git fetch" in a private repo; use 'f' in the files panel to run "git fetch" manually`,
}, &i18n.Message{
ID: "StageLines",
Other: `stage individual hunks/lines`,
}, &i18n.Message{
ID: "FileStagingRequirements",
Other: `Can only stage individual lines for tracked files with unstaged changes`,
}, &i18n.Message{
ID: "StageHunk",
Other: `stage hunk`,
}, &i18n.Message{
ID: "StageLine",
Other: `stage line`,
}, &i18n.Message{
ID: "EscapeStaging",
Other: `return to files panel`,
}, &i18n.Message{
ID: "CantFindHunks",
Other: `Could not find any hunks in this patch`,
}, &i18n.Message{
ID: "CantFindHunk",
Other: `Could not find hunk`,
}, &i18n.Message{
ID: "FastForward",
Other: `fast-forward this branch from its upstream`,
}, &i18n.Message{
ID: "Fetching",
Other: "fetching and fast-forwarding {{.from}} -> {{.to}} ...",
}, &i18n.Message{
ID: "FoundConflicts",
Other: "Conflicts! To abort press 'esc', otherwise press 'enter'",
}, &i18n.Message{
ID: "FoundConflictsTitle",
Other: "Auto-merge failed",
}, &i18n.Message{
ID: "Undo",
Other: "undo",
}, &i18n.Message{
ID: "PickHunk",
Other: "pick hunk",
}, &i18n.Message{
ID: "PickBothHunks",
Other: "pick both hunks",
}, &i18n.Message{
ID: "ViewMergeRebaseOptions",
Other: "view merge/rebase options",
}, &i18n.Message{
ID: "NotMergingOrRebasing",
Other: "You are currently neither rebasing nor merging",
}, &i18n.Message{
ID: "RecentRepos",
Other: "recent repositories",
}, &i18n.Message{
ID: "MergeOptionsTitle",
Other: "Merge Options",
}, &i18n.Message{
ID: "RebaseOptionsTitle",
Other: "Rebase Options",
}, &i18n.Message{
ID: "ConflictsResolved",
Other: "all merge conflicts resolved. Continue?",
}, &i18n.Message{
ID: "RebasingTitle",
Other: "Rebasing",
}, &i18n.Message{
ID: "MergingTitle",
Other: "Merging",
}, &i18n.Message{
ID: "ConfirmRebase",
Other: "Are you sure you want to rebase {{.checkedOutBranch}} onto {{.selectedBranch}}?",
}, &i18n.Message{
ID: "ConfirmMerge",
Other: "Are you sure you want to merge {{.selectedBranch}} into {{.checkedOutBranch}}?",
}, &i18n.Message{}, &i18n.Message{
ID: "FwdNoUpstream",
Other: "Cannot fast-forward a branch with no upstream",
}, &i18n.Message{
ID: "FwdCommitsToPush",
Other: "Cannot fast-forward a branch with commits to push",
}, &i18n.Message{
ID: "ErrorOccurred",
Other: "An error occurred! Please create an issue at https://github.com/jesseduffield/lazygit/issues",
}, &i18n.Message{
ID: "NoRoom",
Other: "Not enough room",
}, &i18n.Message{
ID: "YouAreHere",
Other: "YOU ARE HERE",
}, &i18n.Message{
ID: "rewordNotSupported",
Other: "rewording commits while interactively rebasing is not currently supported",
}, &i18n.Message{
ID: "cherryPickCopy",
Other: "copy commit (cherry-pick)",
}, &i18n.Message{
ID: "cherryPickCopyRange",
Other: "copy commit range (cherry-pick)",
}, &i18n.Message{
ID: "pasteCommits",
Other: "paste commits (cherry-pick)",
}, &i18n.Message{
ID: "SureCherryPick",
Other: "Are you sure you want to cherry-pick the copied commits onto this branch?",
}, &i18n.Message{
ID: "CherryPick",
Other: "Cherry-Pick",
}, &i18n.Message{
ID: "CannotRebaseOntoFirstCommit",
Other: "You cannot interactive rebase onto the first commit",
}, &i18n.Message{
ID: "CannotSquashOntoSecondCommit",
Other: "You cannot squash/fixup onto the second commit",
}, &i18n.Message{
ID: "Donate",
Other: "Donate",
}, &i18n.Message{
ID: "PrevLine",
Other: "select previous line",
}, &i18n.Message{
ID: "NextLine",
Other: "select next line",
}, &i18n.Message{
ID: "PrevHunk",
Other: "select previous hunk",
}, &i18n.Message{
ID: "NextHunk",
Other: "select next hunk",
}, &i18n.Message{
ID: "PrevConflict",
Other: "select previous conflict",
}, &i18n.Message{
ID: "NextConflict",
Other: "select next conflict",
}, &i18n.Message{
ID: "SelectTop",
Other: "select top hunk",
}, &i18n.Message{
ID: "SelectBottom",
Other: "select bottom hunk",
}, &i18n.Message{
ID: "ScrollDown",
Other: "scroll down",
}, &i18n.Message{
ID: "ScrollUp",
Other: "scroll up",
}, &i18n.Message{
ID: "AmendCommitTitle",
Other: "Amend Commit",
}, &i18n.Message{
ID: "AmendCommitPrompt",
Other: "Are you sure you want to amend this commit with your staged files?",
}, &i18n.Message{
ID: "DeleteCommitTitle",
Other: "Delete Commit",
}, &i18n.Message{
ID: "DeleteCommitPrompt",
Other: "Are you sure you want to delete this commit?",
}, &i18n.Message{
ID: "SquashingStatus",
Other: "squashing",
}, &i18n.Message{
ID: "FixingStatus",
Other: "fixing up",
}, &i18n.Message{
ID: "DeletingStatus",
Other: "deleting",
}, &i18n.Message{
ID: "MovingStatus",
Other: "moving",
}, &i18n.Message{
ID: "RebasingStatus",
Other: "rebasing",
}, &i18n.Message{
ID: "AmendingStatus",
Other: "amending",
}, &i18n.Message{
ID: "CherryPickingStatus",
Other: "cherry-picking",
}, &i18n.Message{
ID: "CommitFiles",
Other: "Commit files",
}, &i18n.Message{
ID: "viewCommitFiles",
Other: "view commit's files",
}, &i18n.Message{
ID: "CommitFilesTitle",
Other: "Commit files",
}, &i18n.Message{
ID: "goBack",
Other: "go back",
}, &i18n.Message{
ID: "NoCommiteFiles",
Other: "No files for this commit",
}, &i18n.Message{
ID: "checkoutCommitFile",
Other: "checkout file",
}, &i18n.Message{
ID: "discardOldFileChange",
Other: "discard this commit's changes to this file",
}, &i18n.Message{
ID: "DiscardFileChangesTitle",
Other: "Discard file changes",
}, &i18n.Message{
ID: "DiscardFileChangesPrompt",
Other: "Are you sure you want to discard this commit's changes to this file? If this file was created in this commit, it will be deleted",
}, &i18n.Message{
ID: "DisabledForGPG",
Other: "Feature not available for users using GPG",
}, &i18n.Message{
ID: "CreateRepo",
Other: "Not in a git repository. Create a new git repository? (y/n): ",
}, &i18n.Message{
ID: "AutoStashTitle",
Other: "Autostash?",
}, &i18n.Message{
ID: "AutoStashPrompt",
Other: "You must stash and pop your changes to bring them across. Do this automatically? (enter/esc)",
}, &i18n.Message{
ID: "StashPrefix",
Other: "Auto-stashing changes for ",
}, &i18n.Message{
ID: "viewDiscardOptions",
Other: "view 'discard changes' options",
}, &i18n.Message{
ID: "cancel",
Other: "cancel",
}, &i18n.Message{
ID: "discardAllChanges",
Other: "discard all changes",
}, &i18n.Message{
ID: "discardUnstagedChanges",
Other: "discard unstaged changes",
}, &i18n.Message{
ID: "discardAllChangesToAllFiles",
Other: "nuke working tree",
}, &i18n.Message{
ID: "discardAnyUnstagedChanges",
Other: "discard unstaged changes",
}, &i18n.Message{
ID: "discardUntrackedFiles",
Other: "discard untracked files",
}, &i18n.Message{
ID: "hardReset",
Other: "hard reset",
}, &i18n.Message{
ID: "viewResetOptions",
Other: `view reset options`,
}, &i18n.Message{
ID: "createFixupCommit",
Other: `create fixup commit for this commit`,
}, &i18n.Message{
ID: "squashAboveCommits",
Other: `squash above commits`,
}, &i18n.Message{
ID: "SquashAboveCommits",
Other: `Squash above commits`,
}, &i18n.Message{
ID: "SureSquashAboveCommits",
Other: `Are you sure you want to squash all fixup! commits above {{.commit}}?`,
}, &i18n.Message{
ID: "CreateFixupCommit",
Other: `Create fixup commit`,
}, &i18n.Message{
ID: "SureCreateFixupCommit",
Other: `Are you sure you want to create a fixup! commit for commit {{.commit}}?`,
}, &i18n.Message{
ID: "executeCustomCommand",
Other: "execute custom command",
}, &i18n.Message{
ID: "CustomCommand",
Other: "Custom Command:",
}, &i18n.Message{
ID: "commitChangesWithoutHook",
Other: "commit changes without pre-commit hook",
}, &i18n.Message{
ID: "SkipHookPrefixNotConfigured",
Other: "You have not configured a commit message prefix for skipping hooks. Set `git.skipHookPrefix = 'WIP'` in your config",
}, &i18n.Message{
ID: "resetTo",
Other: `reset to`,
}, &i18n.Message{
ID: "pressEnterToReturn",
Other: "Press enter to return to lazygit",
}, &i18n.Message{
ID: "viewStashOptions",
Other: "view stash options",
}, &i18n.Message{
ID: "stashAllChanges",
Other: "stash changes",
}, &i18n.Message{
ID: "stashStagedChanges",
Other: "stash staged changes",
}, &i18n.Message{
ID: "stashOptions",
Other: "Stash options",
}, &i18n.Message{
ID: "notARepository",
Other: "Error: must be run inside a git repository",
}, &i18n.Message{
ID: "jump",
Other: "jump to panel",
},
)
}
+26 -44
View File
@@ -1,9 +1,9 @@
package i18n
import (
"github.com/Sirupsen/logrus"
"github.com/cloudfoundry/jibber_jabber"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/sirupsen/logrus"
"golang.org/x/text/language"
)
@@ -14,16 +14,34 @@ type Teml map[string]interface{}
type Localizer struct {
i18nLocalizer *i18n.Localizer
language string
Log *logrus.Entry
Log *logrus.Logger
}
// NewLocalizer creates a new Localizer
func NewLocalizer(log *logrus.Entry) *Localizer {
userLang := detectLanguage(jibber_jabber.DetectLanguage)
func NewLocalizer(log *logrus.Logger) (*Localizer, error) {
// detect the user's language
userLang, err := jibber_jabber.DetectLanguage()
if err != nil {
return nil, err
}
log.Info("language: " + userLang)
return setupLocalizer(log, userLang)
// create a i18n bundle that can be used to add translations and other things
i18nBundle := &i18n.Bundle{DefaultLanguage: language.English}
addBundles(i18nBundle)
// return the new localizer that can be used to translate text
i18nLocalizer := i18n.NewLocalizer(i18nBundle, userLang)
localizer := &Localizer{
i18nLocalizer: i18nLocalizer,
language: userLang,
Log: log,
}
return localizer, nil
}
// Localize handels the translations
@@ -60,43 +78,7 @@ func (l *Localizer) GetLanguage() string {
}
// add translation file(s)
func addBundles(log *logrus.Entry, i18nBundle *i18n.Bundle) {
fs := []func(*i18n.Bundle) error{
addPolish,
addDutch,
addEnglish,
}
for _, f := range fs {
if err := f(i18nBundle); err != nil {
log.Fatal(err)
}
}
}
// detectLanguage extracts user language from environment
func detectLanguage(langDetector func() (string, error)) string {
if userLang, err := langDetector(); err == nil {
return userLang
}
return "C"
}
// setupLocalizer creates a new localizer using given userLang
func setupLocalizer(log *logrus.Entry, userLang string) *Localizer {
// create a i18n bundle that can be used to add translations and other things
i18nBundle := i18n.NewBundle(language.English)
addBundles(log, i18nBundle)
// return the new localizer that can be used to translate text
i18nLocalizer := i18n.NewLocalizer(i18nBundle, userLang)
return &Localizer{
i18nLocalizer: i18nLocalizer,
language: userLang,
Log: log,
}
func addBundles(i18nBundle *i18n.Bundle) {
addDutch(i18nBundle)
addEnglish(i18nBundle)
}
-91
View File
@@ -1,91 +0,0 @@
package i18n
import (
"fmt"
"io/ioutil"
"testing"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
func getDummyLog() *logrus.Entry {
log := logrus.New()
log.Out = ioutil.Discard
return log.WithField("test", "test")
}
// TestNewLocalizer is a function.
func TestNewLocalizer(t *testing.T) {
assert.NotNil(t, NewLocalizer(getDummyLog()))
}
// TestDetectLanguage is a function.
func TestDetectLanguage(t *testing.T) {
type scenario struct {
langDetector func() (string, error)
expected string
}
scenarios := []scenario{
{
func() (string, error) {
return "", fmt.Errorf("An error occurred")
},
"C",
},
{
func() (string, error) {
return "en", nil
},
"en",
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, detectLanguage(s.langDetector))
}
}
// TestLocalizer is a function.
func TestLocalizer(t *testing.T) {
type scenario struct {
userLang string
test func(*Localizer)
}
scenarios := []scenario{
{
"C",
func(l *Localizer) {
assert.EqualValues(t, "C", l.GetLanguage())
assert.Equal(t, "Diff", l.Localize(&i18n.LocalizeConfig{
DefaultMessage: &i18n.Message{
ID: "DiffTitle",
},
}))
assert.Equal(t, "Diff", l.SLocalize("DiffTitle"))
assert.Equal(t, "Are you sure you want to delete the branch test?", l.TemplateLocalize("DeleteBranchMessage", Teml{"selectedBranchName": "test"}))
},
},
{
"nl",
func(l *Localizer) {
assert.EqualValues(t, "nl", l.GetLanguage())
assert.Equal(t, "Diff", l.Localize(&i18n.LocalizeConfig{
DefaultMessage: &i18n.Message{
ID: "DiffTitle",
},
}))
assert.Equal(t, "Diff", l.SLocalize("DiffTitle"))
assert.Equal(t, "Weet je zeker dat je branch test wilt verwijderen?", l.TemplateLocalize("DeleteBranchMessage", Teml{"selectedBranchName": "test"}))
},
},
}
for _, s := range scenarios {
s.test(setupLocalizer(getDummyLog(), s.userLang))
}
}
-742
View File
@@ -1,742 +0,0 @@
package i18n
import (
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
)
func addPolish(i18nObject *i18n.Bundle) error {
return i18nObject.AddMessages(language.Polish,
&i18n.Message{
ID: "NotEnoughSpace",
Other: "Za mało miejsca do wyświetlenia paneli",
}, &i18n.Message{
ID: "DiffTitle",
Other: "Różnice",
}, &i18n.Message{
ID: "LogTitle",
Other: "Log",
}, &i18n.Message{
ID: "FilesTitle",
Other: "Pliki",
}, &i18n.Message{
ID: "BranchesTitle",
Other: "Gałęzie",
}, &i18n.Message{
ID: "CommitsTitle",
Other: "Commity",
}, &i18n.Message{
ID: "CommitsDiffTitle",
Other: "Commits (specific diff mode)",
}, &i18n.Message{
ID: "CommitsDiff",
Other: "select commit to diff with another commit",
}, &i18n.Message{
ID: "StashTitle",
Other: "Schowek",
}, &i18n.Message{
ID: "StagingMainTitle",
Other: `Stage Lines/Hunks`,
}, &i18n.Message{
ID: "MergingMainTitle",
Other: "Resolve merge conflicts",
}, &i18n.Message{
ID: "CommitMessage",
Other: "Wiadomość commita",
}, &i18n.Message{
ID: "CredentialsUsername",
Other: "Username",
}, &i18n.Message{
ID: "CredentialsPassword",
Other: "Password",
}, &i18n.Message{
ID: "PassUnameWrong",
Other: "Password and/or username wrong",
}, &i18n.Message{
ID: "CommitChanges",
Other: "commituj zmiany",
}, &i18n.Message{
ID: "AmendLastCommit",
Other: "zmień ostatnie zatwierdzenie",
}, &i18n.Message{
ID: "SureToAmend",
Other: "Czy na pewno chcesz zmienić ostatnie zatwierdzenie? Możesz zmienić komunikat zatwierdzenia z panelu zatwierdzeń.",
}, &i18n.Message{
ID: "NoCommitToAmend",
Other: "Nie ma zobowiązania do zmiany.",
}, &i18n.Message{
ID: "CommitChangesWithEditor",
Other: "commituj zmiany używając edytora z gita",
}, &i18n.Message{
ID: "StatusTitle",
Other: "Status",
}, &i18n.Message{
ID: "GlobalTitle",
Other: "Globalne",
}, &i18n.Message{
ID: "navigate",
Other: "nawiguj",
}, &i18n.Message{
ID: "menu",
Other: "menu",
}, &i18n.Message{
ID: "execute",
Other: "wykonaj",
}, &i18n.Message{
ID: "open",
Other: "otwórz",
}, &i18n.Message{
ID: "ignore",
Other: "ignoruj",
}, &i18n.Message{
ID: "delete",
Other: "usuń",
}, &i18n.Message{
ID: "toggleStaged",
Other: "przełącz zatwierdzenie",
}, &i18n.Message{
ID: "toggleStagedAll",
Other: "przełącz wszystkie zatwierdzenia",
}, &i18n.Message{
ID: "refresh",
Other: "odśwież",
}, &i18n.Message{
ID: "addPatch",
Other: "dodaj łatkę",
}, &i18n.Message{
ID: "edit",
Other: "edytuj",
}, &i18n.Message{
ID: "scroll",
Other: "przewiń",
}, &i18n.Message{
ID: "abortMerge",
Other: "o scalaniu",
}, &i18n.Message{
ID: "resolveMergeConflicts",
Other: "rozwiąż konflikty scalania",
}, &i18n.Message{
ID: "checkout",
Other: "przełącz",
}, &i18n.Message{
ID: "NoChangedFiles",
Other: "Brak zmienionych plików",
}, &i18n.Message{
ID: "FileHasNoUnstagedChanges",
Other: "Plik nie zawiera żadnych nieopublikowanych zmian do dodania",
}, &i18n.Message{
ID: "CannotGitAdd",
Other: "Nie można git add --patch nieśledzonych plików",
}, &i18n.Message{
ID: "CantIgnoreTrackFiles",
Other: "Nie można zignorować nieśledzonych plików",
}, &i18n.Message{
ID: "NoStagedFilesToCommit",
Other: "Brak zatwierdzonych plików do commita",
}, &i18n.Message{
ID: "NoFilesDisplay",
Other: "Brak pliku do wyświetlenia",
}, &i18n.Message{
ID: "PullWait",
Other: "Wciąganie zmian...",
}, &i18n.Message{
ID: "PushWait",
Other: "Wypychanie zmian...",
}, &i18n.Message{
ID: "FetchWait",
Other: "Fetching...",
}, &i18n.Message{
ID: "FileNoMergeCons",
Other: "Ten plik nie powoduje konfliktów scalania",
}, &i18n.Message{
ID: "SureTo",
Other: "Jesteś pewny, że chcesz {{.deleteVerb}} {{.fileName}} (stracisz swoje wprowadzone zmiany)?",
}, &i18n.Message{
ID: "AlreadyCheckedOutBranch",
Other: "Już przęłączono na tą gałąź",
}, &i18n.Message{
ID: "SureForceCheckout",
Other: "Jesteś pewny, że chcesz wymusić przełączenie? Stracisz wszystkie lokalne zmiany",
}, &i18n.Message{
ID: "ForceCheckoutBranch",
Other: "Wymuś przełączenie gałęzi",
}, &i18n.Message{
ID: "BranchName",
Other: "Nazwa gałęzi",
}, &i18n.Message{
ID: "NewBranchNameBranchOff",
Other: "Nazwa nowej gałęzi (gałąź na bazie {{.branchName}})",
}, &i18n.Message{
ID: "CantDeleteCheckOutBranch",
Other: "Nie możesz usunąć obecnej przełączonej gałęzi!",
}, &i18n.Message{
ID: "DeleteBranch",
Other: "Usuń gałąź",
}, &i18n.Message{
ID: "DeleteBranchMessage",
Other: "Jesteś pewien, że chcesz usunąć gałąź {{.selectedBranchName}} ?",
}, &i18n.Message{
ID: "ForceDeleteBranchMessage",
Other: "Na pewno wymusić usunięcie gałęzi {{.selectedBranchName}}?",
}, &i18n.Message{
ID: "rebaseBranch",
Other: "rebase branch",
}, &i18n.Message{
ID: "CantRebaseOntoSelf",
Other: "You cannot rebase a branch onto itself",
}, &i18n.Message{
ID: "CantMergeBranchIntoItself",
Other: "Nie możesz scalić gałęzi do samej siebie",
}, &i18n.Message{
ID: "forceCheckout",
Other: "wymuś przełączenie",
}, &i18n.Message{
ID: "merge",
Other: "scal",
}, &i18n.Message{
ID: "checkoutByName",
Other: "przełącz używając nazwy",
}, &i18n.Message{
ID: "newBranch",
Other: "nowa gałąź",
}, &i18n.Message{
ID: "deleteBranch",
Other: "usuń gałąź",
}, &i18n.Message{
ID: "forceDeleteBranch",
Other: "usuń gałąź (wymuś)",
}, &i18n.Message{
ID: "NoBranchesThisRepo",
Other: "Brak gałęzi dla tego repozytorium",
}, &i18n.Message{
ID: "NoTrackingThisBranch",
Other: "Brak śledzenia dla tej gałęzi",
}, &i18n.Message{
ID: "CommitWithoutMessageErr",
Other: "Nie możesz commitować bez podania wiadomości",
}, &i18n.Message{
ID: "CloseConfirm",
Other: "{{.keyBindClose}}: zamknij, {{.keyBindConfirm}}: potwierdź",
}, &i18n.Message{
ID: "close",
Other: "zamknij",
}, &i18n.Message{
ID: "SureResetThisCommit",
Other: "Jesteś pewny, że chcesz zresetować ten commit?",
}, &i18n.Message{
ID: "ResetToCommit",
Other: "Zresetuj, aby commitować",
}, &i18n.Message{
ID: "squashDown",
Other: "ściśnij w dół",
}, &i18n.Message{
ID: "rename",
Other: "przemianuj",
}, &i18n.Message{
ID: "resetToThisCommit",
Other: "zresetuj do tego commita",
}, &i18n.Message{
ID: "fixupCommit",
Other: "napraw commit",
}, &i18n.Message{
ID: "NoCommitsThisBranch",
Other: "Brak commitów dla tej gałęzi",
}, &i18n.Message{
ID: "OnlySquashTopmostCommit",
Other: "Można tylko ścisnąć najwyższy commit",
}, &i18n.Message{
ID: "YouNoCommitsToSquash",
Other: "Nie masz commitów do ściśnięcia",
}, &i18n.Message{
ID: "CantFixupWhileUnstagedChanges",
Other: "Nie można wykonać naprawy, kiedy istnieją niezatwierdzone zmiany",
}, &i18n.Message{
ID: "Fixup",
Other: "Napraw",
}, &i18n.Message{
ID: "SureFixupThisCommit",
Other: "Jesteś pewny, ze chcesz naprawić ten commit? Commit poniżej zostanie ściśnięty w górę wraz z tym",
}, &i18n.Message{
ID: "OnlyRenameTopCommit",
Other: "Można przmianować tylko najwyższy commit",
}, &i18n.Message{
ID: "renameCommit",
Other: "przemianuj commit",
}, &i18n.Message{
ID: "renameCommitEditor",
Other: "przemianuj commit w edytorze",
}, &i18n.Message{
ID: "PotentialErrInGetselectedCommit",
Other: "potencjalny błąd w getSelected Commit (niedopasowane ui i stan)",
}, &i18n.Message{
ID: "Error",
Other: "Błąd",
}, &i18n.Message{
ID: "resizingPopupPanel",
Other: "skalowanie wyskakującego panelu",
}, &i18n.Message{
ID: "RunningSubprocess",
Other: "uruchomiony podproces",
}, &i18n.Message{
ID: "selectHunk",
Other: "wybierz kawałek",
}, &i18n.Message{
ID: "navigateConflicts",
Other: "nawiguj konflikty",
}, &i18n.Message{
ID: "pickHunk",
Other: "wybierz kawałek",
}, &i18n.Message{
ID: "pickBothHunks",
Other: "wybierz oba kawałki",
}, &i18n.Message{
ID: "undo",
Other: "cofnij",
}, &i18n.Message{
ID: "pop",
Other: "wyciągnij",
}, &i18n.Message{
ID: "drop",
Other: "porzuć",
}, &i18n.Message{
ID: "apply",
Other: "zastosuj",
}, &i18n.Message{
ID: "NoStashEntries",
Other: "Brak pozycji w schowku",
}, &i18n.Message{
ID: "StashDrop",
Other: "Porzuć schowek",
}, &i18n.Message{
ID: "SureDropStashEntry",
Other: "Jesteś pewny, że chcesz porzucić tę pozycję w schowku?",
}, &i18n.Message{
ID: "NoStashTo",
Other: "Brak schowka dla {{.method}}",
}, &i18n.Message{
ID: "NoTrackedStagedFilesStash",
Other: "Nie masz śledzonych/zatwierdzonych plików do przechowania",
}, &i18n.Message{
ID: "StashChanges",
Other: "Przechowaj zmiany",
}, &i18n.Message{
ID: "IssntListOfViews",
Other: "{{.name}} nie jest na liście widoków",
}, &i18n.Message{
ID: "NoViewMachingNewLineFocusedSwitchStatement",
Other: "Brak widoku pasującego do instrukcji przełączania newLineFocused",
}, &i18n.Message{
ID: "newFocusedViewIs",
Other: "nowy skupiony widok to {{.newFocusedView}}",
}, &i18n.Message{
ID: "CantCloseConfirmationPrompt",
Other: "Nie można zamknąć monitu potwierdzenia: {{.error}}",
}, &i18n.Message{
ID: "MergeAborted",
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: "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: "mergeIntoCurrentBranch",
Other: `scal do obecnej gałęzi`,
}, &i18n.Message{
ID: "ConfirmQuit",
Other: `Na pewno chcesz wyjść z programu?`,
}, &i18n.Message{
ID: "UnsupportedGitService",
Other: `Nieobsługiwana usługa git`,
}, &i18n.Message{
ID: "createPullRequest",
Other: `utwórz żądanie wyciągnięcia`,
}, &i18n.Message{
ID: "NoBranchOnRemote",
Other: `Ta gałąź nie istnieje na zdalnym. Najpierw musisz go odepchnąć na odległość.`,
}, &i18n.Message{
ID: "fetch",
Other: `fetch`,
}, &i18n.Message{
ID: "NoAutomaticGitFetchTitle",
Other: `No automatic git fetch`,
}, &i18n.Message{
ID: "NoAutomaticGitFetchBody",
Other: `Lazygit can't use "git fetch" in a private repo use f in the branches panel to run "git fetch" manually`,
}, &i18n.Message{
ID: "StageLines",
Other: `zatwierdź pojedyncze linie`,
}, &i18n.Message{
ID: "FileStagingRequirements",
Other: `Można tylko zatwierdzić pojedyncze linie dla śledzonych plików z niezatwierdzonymi zmianami`,
}, &i18n.Message{
ID: "StagingTitle",
Other: `Zatwierdzanie`,
}, &i18n.Message{
ID: "StageHunk",
Other: `zatwierdź kawałek`,
}, &i18n.Message{
ID: "StageLine",
Other: `zatwierdź linię`,
}, &i18n.Message{
ID: "EscapeStaging",
Other: `wróć do panelu plików`,
}, &i18n.Message{
ID: "CantFindHunks",
Other: `Nie można znaleźć żadnych kawałków w tej łatce`,
}, &i18n.Message{
ID: "CantFindHunk",
Other: `Nie można znaleźć kawałka`,
}, &i18n.Message{
ID: "RebasingTitle",
Other: "Rebasing",
}, &i18n.Message{
ID: "MergingTitle",
Other: "Merging",
}, &i18n.Message{
ID: "ConfirmRebase",
Other: "Are you sure you want to rebase {{.checkedOutBranch}} onto {{.selectedBranch}}?",
}, &i18n.Message{
ID: "ConfirmMerge",
Other: "Are you sure you want to merge {{.selectedBranch}} into {{.checkedOutBranch}}?",
}, &i18n.Message{}, &i18n.Message{
ID: "FwdNoUpstream",
Other: "Cannot fast-forward a branch with no upstream",
}, &i18n.Message{
ID: "FwdCommitsToPush",
Other: "Cannot fast-forward a branch with commits to push",
}, &i18n.Message{
ID: "ErrorOccurred",
Other: "An error occurred! Please create an issue at https://github.com/jesseduffield/lazygit/issues",
}, &i18n.Message{
ID: "MainTitle",
Other: "Main",
}, &i18n.Message{
ID: "NormalTitle",
Other: "Normal",
}, &i18n.Message{
ID: "softReset",
Other: "soft reset",
}, &i18n.Message{
ID: "SureSquashThisCommit",
Other: "Are you sure you want to squash this commit into the commit below?",
}, &i18n.Message{
ID: "Squash",
Other: "Squash",
}, &i18n.Message{
ID: "pickCommit",
Other: "pick commit (when mid-rebase)",
}, &i18n.Message{
ID: "revertCommit",
Other: "revert commit",
}, &i18n.Message{
ID: "deleteCommit",
Other: "delete commit",
}, &i18n.Message{
ID: "moveDownCommit",
Other: "move commit down one",
}, &i18n.Message{
ID: "moveUpCommit",
Other: "move commit up one",
}, &i18n.Message{
ID: "editCommit",
Other: "edit commit",
}, &i18n.Message{
ID: "amendToCommit",
Other: "amend commit with staged changes",
}, &i18n.Message{
ID: "FoundConflicts",
Other: "Conflicts! To abort press 'esc', otherwise press 'enter'",
}, &i18n.Message{
ID: "FoundConflictsTitle",
Other: "Auto-merge failed",
}, &i18n.Message{
ID: "Undo",
Other: "undo",
}, &i18n.Message{
ID: "PickHunk",
Other: "pick hunk",
}, &i18n.Message{
ID: "PickBothHunks",
Other: "pick both hunks",
}, &i18n.Message{
ID: "ViewMergeRebaseOptions",
Other: "view merge/rebase options",
}, &i18n.Message{
ID: "NotMergingOrRebasing",
Other: "You are currently neither rebasing nor merging",
}, &i18n.Message{
ID: "RecentRepos",
Other: "recent repositories",
}, &i18n.Message{
ID: "MergeOptionsTitle",
Other: "Merge Options",
}, &i18n.Message{
ID: "RebaseOptionsTitle",
Other: "Rebase Options",
}, &i18n.Message{
ID: "ConflictsResolved",
Other: "all merge conflicts resolved. Continue?",
}, &i18n.Message{
ID: "NoRoom",
Other: "Not enough room",
}, &i18n.Message{
ID: "YouAreHere",
Other: "YOU ARE HERE",
}, &i18n.Message{
ID: "rewordNotSupported",
Other: "rewording commits while interactively rebasing is not currently supported",
}, &i18n.Message{
ID: "cherryPickCopy",
Other: "copy commit (cherry-pick)",
}, &i18n.Message{
ID: "cherryPickCopyRange",
Other: "copy commit range (cherry-pick)",
}, &i18n.Message{
ID: "pasteCommits",
Other: "paste commits (cherry-pick)",
}, &i18n.Message{
ID: "SureCherryPick",
Other: "Are you sure you want to cherry-pick the copied commits onto this branch?",
}, &i18n.Message{
ID: "CherryPick",
Other: "Cherry-Pick",
}, &i18n.Message{
ID: "CannotRebaseOntoFirstCommit",
Other: "You cannot interactive rebase onto the first commit",
}, &i18n.Message{
ID: "CannotSquashOntoSecondCommit",
Other: "You cannot squash/fixup onto the second commit",
}, &i18n.Message{
ID: "Donate",
Other: "Donate",
}, &i18n.Message{
ID: "PrevLine",
Other: "select previous line",
}, &i18n.Message{
ID: "NextLine",
Other: "select next line",
}, &i18n.Message{
ID: "PrevHunk",
Other: "select previous hunk",
}, &i18n.Message{
ID: "NextHunk",
Other: "select next hunk",
}, &i18n.Message{
ID: "PrevConflict",
Other: "select previous conflict",
}, &i18n.Message{
ID: "NextConflict",
Other: "select next conflict",
}, &i18n.Message{
ID: "SelectTop",
Other: "select top hunk",
}, &i18n.Message{
ID: "SelectBottom",
Other: "select bottom hunk",
}, &i18n.Message{
ID: "ScrollDown",
Other: "scroll down",
}, &i18n.Message{
ID: "ScrollUp",
Other: "scroll up",
}, &i18n.Message{
ID: "AmendCommitTitle",
Other: "Amend Commit",
}, &i18n.Message{
ID: "AmendCommitPrompt",
Other: "Are you sure you want to amend this commit with your staged files?",
}, &i18n.Message{
ID: "DeleteCommitTitle",
Other: "Delete Commit",
}, &i18n.Message{
ID: "DeleteCommitPrompt",
Other: "Are you sure you want to delete this commit?",
}, &i18n.Message{
ID: "SquashingStatus",
Other: "squashing",
}, &i18n.Message{
ID: "FixingStatus",
Other: "fixing up",
}, &i18n.Message{
ID: "DeletingStatus",
Other: "deleting",
}, &i18n.Message{
ID: "MovingStatus",
Other: "moving",
}, &i18n.Message{
ID: "RebasingStatus",
Other: "rebasing",
}, &i18n.Message{
ID: "AmendingStatus",
Other: "amending",
}, &i18n.Message{
ID: "CherryPickingStatus",
Other: "cherry-picking",
}, &i18n.Message{
ID: "CommitFiles",
Other: "Commit files",
}, &i18n.Message{
ID: "viewCommitFiles",
Other: "view commit's files",
}, &i18n.Message{
ID: "CommitFilesTitle",
Other: "Commit files",
}, &i18n.Message{
ID: "goBack",
Other: "go back",
}, &i18n.Message{
ID: "NoCommiteFiles",
Other: "No files for this commit",
}, &i18n.Message{
ID: "checkoutCommitFile",
Other: "checkout file",
}, &i18n.Message{
ID: "discardOldFileChange",
Other: "discard this commit's changes to this file",
}, &i18n.Message{
ID: "DiscardFileChangesTitle",
Other: "Discard file changes",
}, &i18n.Message{
ID: "DiscardFileChangesPrompt",
Other: "Are you sure you want to discard this commit's changes to this file? If this file was created in this commit, it will be deleted",
}, &i18n.Message{
ID: "DisabledForGPG",
Other: "Feature not available for users using GPG",
}, &i18n.Message{
ID: "CreateRepo",
Other: "Not in a git repository. Create a new git repository? (y/n): ",
}, &i18n.Message{
ID: "AutoStashTitle",
Other: "Autostash?",
}, &i18n.Message{
ID: "AutoStashPrompt",
Other: "You must stash and pop your changes to bring them across. Do this automatically? (enter/esc)",
}, &i18n.Message{
ID: "StashPrefix",
Other: "Auto-stashing changes for ",
}, &i18n.Message{
ID: "viewDiscardOptions",
Other: "view 'discard changes' options",
}, &i18n.Message{
ID: "cancel",
Other: "cancel",
}, &i18n.Message{
ID: "discardAllChanges",
Other: "discard all changes",
}, &i18n.Message{
ID: "discardUnstagedChanges",
Other: "discard unstaged changes",
}, &i18n.Message{
ID: "discardAllChangesToAllFiles",
Other: "nuke working tree",
}, &i18n.Message{
ID: "discardAnyUnstagedChanges",
Other: "discard unstaged changes",
}, &i18n.Message{
ID: "discardUntrackedFiles",
Other: "discard untracked files",
}, &i18n.Message{
ID: "hardReset",
Other: "hard reset",
}, &i18n.Message{
ID: "viewResetOptions",
Other: `view reset options`,
}, &i18n.Message{
ID: "createFixupCommit",
Other: `create fixup commit for this commit`,
}, &i18n.Message{
ID: "squashAboveCommits",
Other: `squash above commits`,
}, &i18n.Message{
ID: "SquashAboveCommits",
Other: `Squash above commits`,
}, &i18n.Message{
ID: "SureSquashAboveCommits",
Other: `Are you sure you want to squash all fixup! commits above {{.commit}}?`,
}, &i18n.Message{
ID: "CreateFixupCommit",
Other: `Create fixup commit`,
}, &i18n.Message{
ID: "SureCreateFixupCommit",
Other: `Are you sure you want to create a fixup! commit for commit {{.commit}}?`,
}, &i18n.Message{
ID: "executeCustomCommand",
Other: "execute custom command",
}, &i18n.Message{
ID: "CustomCommand",
Other: "Custom Command:",
}, &i18n.Message{
ID: "commitChangesWithoutHook",
Other: "commit changes without pre-commit hook",
}, &i18n.Message{
ID: "SkipHookPrefixNotConfigured",
Other: "You have not configured a commit message prefix for skipping hooks. Set `git.skipHookPrefix = 'WIP'` in your config",
}, &i18n.Message{
ID: "resetTo",
Other: `reset to`,
}, &i18n.Message{
ID: "pressEnterToReturn",
Other: "Press enter to return to lazygit",
}, &i18n.Message{
ID: "viewStashOptions",
Other: "view stash options",
}, &i18n.Message{
ID: "stashAllChanges",
Other: "przechowaj pliki",
}, &i18n.Message{
ID: "stashStagedChanges",
Other: "stash staged changes",
}, &i18n.Message{
ID: "stashOptions",
Other: "Stash options",
}, &i18n.Message{
ID: "notARepository",
Other: "Error: must be run inside a git repository",
}, &i18n.Message{
ID: "jump",
Other: "jump to panel",
},
)
}
-31
View File
@@ -1,31 +0,0 @@
package test
import (
"github.com/go-errors/errors"
"os"
"os/exec"
"path/filepath"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// GenerateRepo generates a repo from test/repos and changes the directory to be
// inside the newly made repo
func GenerateRepo(filename string) error {
reposDir := "/test/repos/"
testPath := utils.GetProjectRoot() + reposDir
// workaround for debian packaging
if _, err := os.Stat(testPath); os.IsNotExist(err) {
cwd, _ := os.Getwd()
testPath = filepath.Dir(filepath.Dir(cwd)) + reposDir
}
if err := os.Chdir(testPath); err != nil {
return err
}
if output, err := exec.Command("bash", filename).CombinedOutput(); err != nil {
return errors.New(string(output))
}
return os.Chdir(testPath + "repo")
}
-45
View File
@@ -1,45 +0,0 @@
package test
import (
"fmt"
"os/exec"
"strings"
"testing"
"github.com/mgutz/str"
"github.com/stretchr/testify/assert"
)
// CommandSwapper takes a command, verifies that it is what it's expected to be
// and then returns a replacement command that will actually be called by the os
type CommandSwapper struct {
Expect string
Replace string
}
// SwapCommand verifies the command is what we expected, and swaps it out for a different command
func (i *CommandSwapper) SwapCommand(t *testing.T, cmd string, args []string) *exec.Cmd {
splitCmd := str.ToArgv(i.Expect)
assert.EqualValues(t, splitCmd[0], cmd, fmt.Sprintf("received command: %s %s", cmd, strings.Join(args, " ")))
if len(splitCmd) > 1 {
assert.EqualValues(t, splitCmd[1:], args, fmt.Sprintf("received command: %s %s", cmd, strings.Join(args, " ")))
}
splitCmd = str.ToArgv(i.Replace)
return exec.Command(splitCmd[0], splitCmd[1:]...)
}
// CreateMockCommand creates a command function that will verify its receiving the right sequence of commands from lazygit
func CreateMockCommand(t *testing.T, swappers []*CommandSwapper) func(cmd string, args ...string) *exec.Cmd {
commandIndex := 0
return func(cmd string, args ...string) *exec.Cmd {
var command *exec.Cmd
if commandIndex > len(swappers)-1 {
assert.Fail(t, fmt.Sprintf("too many commands run. This command was (%s %s)", cmd, strings.Join(args, " ")))
}
command = swappers[commandIndex].SwapCommand(t, cmd, args)
commandIndex++
return command
}
}
-100
View File
@@ -1,100 +0,0 @@
package theme
import (
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/spf13/viper"
)
var (
// DefaultTextColor is the default text color
DefaultTextColor = color.FgWhite
// GocuiDefaultTextColor does the same as DefaultTextColor but this one only colors gocui default text colors
GocuiDefaultTextColor gocui.Attribute
// ActiveBorderColor is the border color of the active frame
ActiveBorderColor gocui.Attribute
// InactiveBorderColor is the border color of the inactive active frames
InactiveBorderColor gocui.Attribute
)
// UpdateTheme updates all theme variables
func UpdateTheme(userConfig *viper.Viper) {
ActiveBorderColor = getColor(userConfig.GetStringSlice("gui.theme.activeBorderColor"))
InactiveBorderColor = getColor(userConfig.GetStringSlice("gui.theme.inactiveBorderColor"))
isLightTheme := userConfig.GetBool("gui.theme.lightTheme")
if isLightTheme {
DefaultTextColor = color.FgBlack
GocuiDefaultTextColor = gocui.ColorBlack
} else {
DefaultTextColor = color.FgWhite
GocuiDefaultTextColor = gocui.ColorWhite
}
}
// getAttribute gets the gocui color attribute from the string
func getAttribute(key string) gocui.Attribute {
colorMap := map[string]gocui.Attribute{
"default": gocui.ColorDefault,
"black": gocui.ColorBlack,
"red": gocui.ColorRed,
"green": gocui.ColorGreen,
"yellow": gocui.ColorYellow,
"blue": gocui.ColorBlue,
"magenta": gocui.ColorMagenta,
"cyan": gocui.ColorCyan,
"white": gocui.ColorWhite,
"bold": gocui.AttrBold,
"reverse": gocui.AttrReverse,
"underline": gocui.AttrUnderline,
}
value, present := colorMap[key]
if present {
return value
}
return gocui.ColorWhite
}
// getColor bitwise OR's a list of attributes obtained via the given keys
func getColor(keys []string) gocui.Attribute {
var attribute gocui.Attribute
for _, key := range keys {
attribute |= getAttribute(key)
}
return attribute
}
// GetAttribute gets the gocui color attribute from the string
func GetAttribute(key string) gocui.Attribute {
colorMap := map[string]gocui.Attribute{
"default": gocui.ColorDefault,
"black": gocui.ColorBlack,
"red": gocui.ColorRed,
"green": gocui.ColorGreen,
"yellow": gocui.ColorYellow,
"blue": gocui.ColorBlue,
"magenta": gocui.ColorMagenta,
"cyan": gocui.ColorCyan,
"white": gocui.ColorWhite,
"bold": gocui.AttrBold,
"reverse": gocui.AttrReverse,
"underline": gocui.AttrUnderline,
}
value, present := colorMap[key]
if present {
return value
}
return gocui.ColorWhite
}
// GetColor bitwise OR's a list of attributes obtained via the given keys
func GetColor(keys []string) gocui.Attribute {
var attribute gocui.Attribute
for _, key := range keys {
attribute |= GetAttribute(key)
}
return attribute
}
-308
View File
@@ -1,308 +0,0 @@
package updates
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/go-errors/errors"
"github.com/kardianos/osext"
"github.com/jesseduffield/go-getter"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/sirupsen/logrus"
)
// Updater checks for updates and does updates
type Updater struct {
Log *logrus.Entry
Config config.AppConfigurer
OSCommand *commands.OSCommand
Tr *i18n.Localizer
}
// Updaterer implements the check and update methods
type Updaterer interface {
CheckForNewUpdate()
Update()
}
const (
PROJECT_URL = "https://github.com/jesseduffield/lazygit"
)
// NewUpdater creates a new updater
func NewUpdater(log *logrus.Entry, config config.AppConfigurer, osCommand *commands.OSCommand, tr *i18n.Localizer) (*Updater, error) {
contextLogger := log.WithField("context", "updates")
return &Updater{
Log: contextLogger,
Config: config,
OSCommand: osCommand,
Tr: tr,
}, nil
}
func (u *Updater) getLatestVersionNumber() (string, error) {
req, err := http.NewRequest("GET", PROJECT_URL+"/releases/latest", nil)
if err != nil {
return "", err
}
req.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
dec := json.NewDecoder(resp.Body)
data := struct {
TagName string `json:"tag_name"`
}{}
if err := dec.Decode(&data); err != nil {
return "", err
}
return data.TagName, nil
}
// RecordLastUpdateCheck records last time an update check was performed
func (u *Updater) RecordLastUpdateCheck() error {
u.Config.GetAppState().LastUpdateCheck = time.Now().Unix()
return u.Config.SaveAppState()
}
// expecting version to be of the form `v12.34.56`
func (u *Updater) majorVersionDiffers(oldVersion, newVersion string) bool {
if oldVersion == "unversioned" {
return false
}
oldVersion = strings.TrimPrefix(oldVersion, "v")
newVersion = strings.TrimPrefix(newVersion, "v")
return strings.Split(oldVersion, ".")[0] != strings.Split(newVersion, ".")[0]
}
func (u *Updater) checkForNewUpdate() (string, error) {
u.Log.Info("Checking for an updated version")
currentVersion := u.Config.GetVersion()
if err := u.RecordLastUpdateCheck(); err != nil {
return "", err
}
newVersion, err := u.getLatestVersionNumber()
if err != nil {
return "", err
}
u.Log.Info("Current version is " + currentVersion)
u.Log.Info("New version is " + newVersion)
if newVersion == currentVersion {
return "", errors.New(u.Tr.SLocalize("OnLatestVersionErr"))
}
if u.majorVersionDiffers(currentVersion, newVersion) {
errMessage := u.Tr.TemplateLocalize(
"MajorVersionErr",
i18n.Teml{
"newVersion": newVersion,
"currentVersion": currentVersion,
},
)
return "", errors.New(errMessage)
}
rawUrl, err := u.getBinaryUrl(newVersion)
if err != nil {
return "", err
}
u.Log.Info("Checking for resource at url " + rawUrl)
if !u.verifyResourceFound(rawUrl) {
errMessage := u.Tr.TemplateLocalize(
"CouldNotFindBinaryErr",
i18n.Teml{
"url": rawUrl,
},
)
return "", errors.New(errMessage)
}
u.Log.Info("Verified resource is available, ready to update")
return newVersion, nil
}
// CheckForNewUpdate checks if there is an available update
func (u *Updater) CheckForNewUpdate(onFinish func(string, error) error, userRequested bool) {
if !userRequested && u.skipUpdateCheck() {
return
}
go func() {
newVersion, err := u.checkForNewUpdate()
if err = onFinish(newVersion, err); err != nil {
u.Log.Error(err)
}
}()
}
func (u *Updater) skipUpdateCheck() bool {
// will remove the check for windows after adding a manifest file asking for
// the required permissions
if runtime.GOOS == "windows" {
u.Log.Info("Updating is currently not supported for windows until we can fix permission issues")
return true
}
if u.Config.GetVersion() == "unversioned" {
u.Log.Info("Current version is not built from an official release so we won't check for an update")
return true
}
if u.Config.GetBuildSource() != "buildBinary" {
u.Log.Info("Binary is not built with the buildBinary flag so we won't check for an update")
return true
}
userConfig := u.Config.GetUserConfig()
if userConfig.Get("update.method") == "never" {
u.Log.Info("Update method is set to never so we won't check for an update")
return true
}
currentTimestamp := time.Now().Unix()
lastUpdateCheck := u.Config.GetAppState().LastUpdateCheck
days := userConfig.GetInt64("update.days")
if (currentTimestamp-lastUpdateCheck)/(60*60*24) < days {
u.Log.Info("Last update was too recent so we won't check for an update")
return true
}
return false
}
func (u *Updater) mappedOs(os string) string {
osMap := map[string]string{
"darwin": "Darwin",
"linux": "Linux",
"windows": "Windows",
}
result, found := osMap[os]
if found {
return result
}
return os
}
func (u *Updater) mappedArch(arch string) string {
archMap := map[string]string{
"386": "32-bit",
"amd64": "x86_64",
}
result, found := archMap[arch]
if found {
return result
}
return arch
}
// example: https://github.com/jesseduffield/lazygit/releases/download/v0.1.73/lazygit_0.1.73_Darwin_x86_64.tar.gz
func (u *Updater) getBinaryUrl(newVersion string) (string, error) {
extension := "tar.gz"
if runtime.GOOS == "windows" {
extension = "zip"
}
url := fmt.Sprintf(
"%s/releases/download/%s/lazygit_%s_%s_%s.%s",
PROJECT_URL,
newVersion,
newVersion[1:],
u.mappedOs(runtime.GOOS),
u.mappedArch(runtime.GOARCH),
extension,
)
u.Log.Info("Url for latest release is " + url)
return url, nil
}
// Update downloads the latest binary and replaces the current binary with it
func (u *Updater) Update(newVersion string, onFinish func(error) error) {
go func() {
err := u.update(newVersion)
if err = onFinish(err); err != nil {
u.Log.Error(err)
}
}()
}
func (u *Updater) update(newVersion string) error {
rawUrl, err := u.getBinaryUrl(newVersion)
if err != nil {
return err
}
u.Log.Info("Updating with url " + rawUrl)
return u.downloadAndInstall(rawUrl)
}
func (u *Updater) downloadAndInstall(rawUrl string) error {
url, err := url.Parse(rawUrl)
if err != nil {
return err
}
g := new(getter.HttpGetter)
configDir := u.Config.GetUserConfigDir()
u.Log.Info("Download directory is " + configDir)
// Get it!
if err := g.Get(configDir, url); err != nil {
return err
}
// get the path of the current binary
binaryPath, err := osext.Executable()
if err != nil {
return err
}
u.Log.Info("Binary path is " + binaryPath)
binaryName := filepath.Base(binaryPath)
u.Log.Info("Binary name is " + binaryName)
// Verify the main file exists
tempPath := filepath.Join(configDir, binaryName)
u.Log.Info("Temp path to binary is " + tempPath)
if _, err := os.Stat(tempPath); err != nil {
return err
}
// swap out the old binary for the new one
err = os.Rename(tempPath, binaryPath)
if err != nil {
return err
}
u.Log.Info("Update complete!")
return nil
}
func (u *Updater) verifyResourceFound(rawUrl string) bool {
resp, err := http.Head(rawUrl)
if err != nil {
return false
}
defer resp.Body.Close()
u.Log.Info("Received status code ", resp.StatusCode)
// 403 means the resource is there (not going to bother adding extra request headers)
// 404 means its not
return resp.StatusCode == 403
}
+2 -190
View File
@@ -1,17 +1,11 @@
package utils
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"reflect"
"regexp"
"strings"
"time"
"github.com/go-errors/errors"
"github.com/fatih/color"
)
@@ -33,11 +27,10 @@ func SplitLines(multilineString string) []string {
// WithPadding pads a string as much as you want
func WithPadding(str string, padding int) string {
uncoloredStr := Decolorise(str)
if padding < len(uncoloredStr) {
if padding-len(str) < 0 {
return str
}
return str + strings.Repeat(" ", padding-len(uncoloredStr))
return str + strings.Repeat(" ", padding-len(str))
}
// ColoredString takes a string and a colour attribute and returns a colored
@@ -70,184 +63,3 @@ func TrimTrailingNewline(str string) string {
}
return str
}
// NormalizeLinefeeds - Removes all Windows and Mac style line feeds
func NormalizeLinefeeds(str string) string {
str = strings.Replace(str, "\r\n", "\n", -1)
str = strings.Replace(str, "\r", "", -1)
return str
}
// GetProjectRoot returns the path to the root of the project. Only to be used
// in testing contexts, as with binaries it's unlikely this path will exist on
// the machine
func GetProjectRoot() string {
dir, err := os.Getwd()
if err != nil {
panic(err)
}
return strings.Split(dir, "lazygit")[0] + "lazygit"
}
// Loader dumps a string to be displayed as a loader
func Loader() string {
characters := "|/-\\"
now := time.Now()
nanos := now.UnixNano()
index := nanos / 50000000 % int64(len(characters))
return characters[index : index+1]
}
// ResolvePlaceholderString populates a template with values
func ResolvePlaceholderString(str string, arguments map[string]string) string {
for key, value := range arguments {
str = strings.Replace(str, "{{"+key+"}}", value, -1)
}
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(bool) []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{}, isFocused bool) (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, isFocused)
}
// 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, isFocused bool) (string, error) {
if len(items) == 0 {
return "", nil
}
stringArrays := getDisplayStringArrays(items, isFocused)
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
}
// Decolorise strips a string of color
func Decolorise(str string) string {
re := regexp.MustCompile(`\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]`)
return re.ReplaceAllString(str, "")
}
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 {
uncoloredString := Decolorise(strings[i])
if len(uncoloredString) > padWidths[i] {
padWidths[i] = len(uncoloredString)
}
}
}
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, isFocused bool) [][]string {
stringArrays := make([][]string, len(displayables))
for i, item := range displayables {
stringArrays[i] = item.GetDisplayStrings(isFocused)
}
return stringArrays
}
// IncludesString if the list contains the string
func IncludesString(list []string, a string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
// NextIndex returns the index of the element that comes after the given number
func NextIndex(numbers []int, currentNumber int) int {
for index, number := range numbers {
if number > currentNumber {
return index
}
}
return 0
}
// PrevIndex returns the index that comes before the given number, cycling if we reach the end
func PrevIndex(numbers []int, currentNumber int) int {
end := len(numbers) - 1
for i := end; i >= 0; i -= 1 {
if numbers[i] < currentNumber {
return i
}
}
return end
}
func AsJson(i interface{}) string {
bytes, _ := json.MarshalIndent(i, "", " ")
return string(bytes)
}
-575
View File
@@ -1,575 +0,0 @@
package utils
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestSplitLines is a function.
func TestSplitLines(t *testing.T) {
type scenario struct {
multilineString string
expected []string
}
scenarios := []scenario{
{
"",
[]string{},
},
{
"\n",
[]string{},
},
{
"hello world !\nhello universe !\n",
[]string{
"hello world !",
"hello universe !",
},
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, SplitLines(s.multilineString))
}
}
// TestWithPadding is a function.
func TestWithPadding(t *testing.T) {
type scenario struct {
str string
padding int
expected string
}
scenarios := []scenario{
{
"hello world !",
1,
"hello world !",
},
{
"hello world !",
14,
"hello world ! ",
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, WithPadding(s.str, s.padding))
}
}
// TestTrimTrailingNewline is a function.
func TestTrimTrailingNewline(t *testing.T) {
type scenario struct {
str string
expected string
}
scenarios := []scenario{
{
"hello world !\n",
"hello world !",
},
{
"hello world !",
"hello world !",
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, TrimTrailingNewline(s.str))
}
}
// TestNormalizeLinefeeds is a function.
func TestNormalizeLinefeeds(t *testing.T) {
type scenario struct {
byteArray []byte
expected []byte
}
var scenarios = []scenario{
{
// \r\n
[]byte{97, 115, 100, 102, 13, 10},
[]byte{97, 115, 100, 102, 10},
},
{
// bash\r\nblah
[]byte{97, 115, 100, 102, 13, 10, 97, 115, 100, 102},
[]byte{97, 115, 100, 102, 10, 97, 115, 100, 102},
},
{
// \r
[]byte{97, 115, 100, 102, 13},
[]byte{97, 115, 100, 102},
},
{
// \n
[]byte{97, 115, 100, 102, 10},
[]byte{97, 115, 100, 102, 10},
},
}
for _, s := range scenarios {
assert.EqualValues(t, string(s.expected), NormalizeLinefeeds(string(s.byteArray)))
}
}
// TestResolvePlaceholderString is a function.
func TestResolvePlaceholderString(t *testing.T) {
type scenario struct {
templateString string
arguments map[string]string
expected string
}
scenarios := []scenario{
{
"",
map[string]string{},
"",
},
{
"hello",
map[string]string{},
"hello",
},
{
"hello {{arg}}",
map[string]string{},
"hello {{arg}}",
},
{
"hello {{arg}}",
map[string]string{"arg": "there"},
"hello there",
},
{
"hello",
map[string]string{"arg": "there"},
"hello",
},
{
"{{nothing}}",
map[string]string{"nothing": ""},
"",
},
{
"{{}} {{ this }} { should not throw}} an {{{{}}}} error",
map[string]string{
"blah": "blah",
"this": "won't match",
},
"{{}} {{ this }} { should not throw}} an {{{{}}}} error",
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, ResolvePlaceholderString(s.templateString, s.arguments))
}
}
// TestDisplayArraysAligned is a function.
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{}
// GetDisplayStrings is a function.
func (d *myDisplayable) GetDisplayStrings(isFocused bool) []string {
if isFocused {
return append(d.strings, "blah")
}
return d.strings
}
// TestGetDisplayStringArrays is a function.
func TestGetDisplayStringArrays(t *testing.T) {
type scenario struct {
input []Displayable
isFocused bool
expected [][]string
}
scenarios := []scenario{
{
[]Displayable{
Displayable(&myDisplayable{[]string{"a", "b"}}),
Displayable(&myDisplayable{[]string{"c", "d"}}),
},
false,
[][]string{{"a", "b"}, {"c", "d"}},
},
{
[]Displayable{
Displayable(&myDisplayable{[]string{"a", "b"}}),
Displayable(&myDisplayable{[]string{"c", "d"}}),
},
true,
[][]string{{"a", "b", "blah"}, {"c", "d", "blah"}},
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, getDisplayStringArrays(s.input, s.isFocused))
}
}
// TestRenderDisplayableList is a function.
func TestRenderDisplayableList(t *testing.T) {
type scenario struct {
input []Displayable
isFocused bool
expectedString string
expectedErrorMessage string
}
scenarios := []scenario{
{
[]Displayable{
Displayable(&myDisplayable{[]string{}}),
Displayable(&myDisplayable{[]string{}}),
},
false,
"\n",
"",
},
{
[]Displayable{
Displayable(&myDisplayable{[]string{"aa", "b"}}),
Displayable(&myDisplayable{[]string{"c", "d"}}),
},
false,
"aa b\nc d",
"",
},
{
[]Displayable{
Displayable(&myDisplayable{[]string{"a"}}),
Displayable(&myDisplayable{[]string{"b", "c"}}),
},
false,
"",
"Each item must return the same number of strings to display",
},
{
[]Displayable{
Displayable(&myDisplayable{[]string{"a"}}),
Displayable(&myDisplayable{[]string{"b"}}),
},
true,
"a blah\nb blah",
"",
},
}
for _, s := range scenarios {
str, err := renderDisplayableList(s.input, s.isFocused)
assert.EqualValues(t, s.expectedString, str)
if s.expectedErrorMessage != "" {
assert.EqualError(t, err, s.expectedErrorMessage)
} else {
assert.NoError(t, err)
}
}
}
// TestRenderList is a function.
func TestRenderList(t *testing.T) {
type scenario struct {
input interface{}
isFocused bool
expectedString string
expectedErrorMessage string
}
scenarios := []scenario{
{
[]*myDisplayable{
{[]string{"aa", "b"}},
{[]string{"c", "d"}},
},
false,
"aa b\nc d",
"",
},
{
[]*myStruct{
{},
{},
},
false,
"",
"item does not implement the Displayable interface",
},
{
&myStruct{},
false,
"",
"RenderList given a non-slice type",
},
{
[]*myDisplayable{
{[]string{"a"}},
},
true,
"a blah",
"",
},
}
for _, s := range scenarios {
str, err := RenderList(s.input, s.isFocused)
assert.EqualValues(t, s.expectedString, str)
if s.expectedErrorMessage != "" {
assert.EqualError(t, err, s.expectedErrorMessage)
} else {
assert.NoError(t, err)
}
}
}
// TestGetPaddedDisplayStrings is a function.
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))
}
}
// TestGetPadWidths is a function.
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))
}
}
// TestMin is a function.
func TestMin(t *testing.T) {
type scenario struct {
a int
b int
expected int
}
scenarios := []scenario{
{
1,
1,
1,
},
{
1,
2,
1,
},
{
2,
1,
1,
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, Min(s.a, s.b))
}
}
// TestIncludesString is a function.
func TestIncludesString(t *testing.T) {
type scenario struct {
list []string
element string
expected bool
}
scenarios := []scenario{
{
[]string{"a", "b"},
"a",
true,
},
{
[]string{"a", "b"},
"c",
false,
},
{
[]string{"a", "b"},
"",
false,
},
{
[]string{""},
"",
true,
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, IncludesString(s.list, s.element))
}
}
func TestNextIndex(t *testing.T) {
type scenario struct {
testName string
list []int
element int
expected int
}
scenarios := []scenario{
{
// I'm not really fussed about how it behaves here
"no elements",
[]int{},
1,
0,
},
{
"one element",
[]int{1},
1,
0,
},
{
"two elements",
[]int{1, 2},
1,
1,
},
{
"two elements, giving second one",
[]int{1, 2},
2,
0,
},
{
"three elements, giving second one",
[]int{1, 2, 3},
2,
2,
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
assert.EqualValues(t, s.expected, NextIndex(s.list, s.element))
})
}
}
func TestPrevIndex(t *testing.T) {
type scenario struct {
testName string
list []int
element int
expected int
}
scenarios := []scenario{
{
// I'm not really fussed about how it behaves here
"no elements",
[]int{},
1,
-1,
},
{
"one element",
[]int{1},
1,
0,
},
{
"two elements",
[]int{1, 2},
1,
1,
},
{
"three elements, giving second one",
[]int{1, 2, 3},
2,
0,
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
assert.EqualValues(t, s.expected, PrevIndex(s.list, s.element))
})
}
}
func TestAsJson(t *testing.T) {
type myStruct struct {
a string
}
output := AsJson(&myStruct{a: "foo"})
// no idea why this is returning empty hashes but it's works in the app ¯\_(ツ)_/¯
assert.EqualValues(t, "{}", output)
}
-5
View File
@@ -1,5 +0,0 @@
#!/bin/sh
GO111MODULE=on
mv go.mod /tmp/
go mod init

Some files were not shown because too many files have changed in this diff Show More