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
1163 changed files with 98408 additions and 156389 deletions
+26
View File
@@ -0,0 +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:
# 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
# specify any bash command here prefixed with `run: `
- run: go test -v ./...
- run: bash <(curl -s https://codecov.io/bash)
-5
View File
@@ -1,5 +0,0 @@
# These are supported funding model platforms
github: [jesseduffield]
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: ''
---
-28
View File
@@ -1,28 +0,0 @@
name: automerge
on:
pull_request:
types:
- labeled
- unlabeled
- synchronize
- opened
- edited
- ready_for_review
- reopened
- unlocked
pull_request_review:
types:
- submitted
check_suite:
types:
- completed
status: {}
jobs:
automerge:
runs-on: ubuntu-latest
steps:
- name: automerge
uses: "pascalgn/automerge-action@135f0bdb927d9807b5446f7ca9ecc2c51de03c4a"
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
MERGE_METHOD: rebase
-28
View File
@@ -1,28 +0,0 @@
name: Continuous Delivery
on:
push:
tags:
- 'v*'
jobs:
cd:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Unshallow repo
run: git fetch --prune --unshallow
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.14.x
- name: Run goreleaser
uses: goreleaser/goreleaser-action@v1
env:
GITHUB_TOKEN: ${{secrets.GITHUB_API_TOKEN}}
- name: Bump Homebrew
uses: dawidd6/action-homebrew-bump-formula@v2
with:
token: ${{secrets.GITHUB_API_TOKEN}}
formula: lazygit
-36
View File
@@ -1,36 +0,0 @@
name: Continuous Integration
on: push
jobs:
ci:
runs-on: ubuntu-latest
env:
GOFLAGS: -mod=vendor
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.14.x
- name: Cache build
uses: actions/cache@v1
with:
path: ~/.cache/go-build
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}
restore-keys: |
${{runner.os}}-go-
- name: Format code
run: |
if [ $(find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;|wc -l) -gt 0 ]; then
find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;
exit 1
fi
- name: Test code
run: |
./test.sh
- name: Build binaries
uses: goreleaser/goreleaser-action@v1
with:
args: --skip-publish --snapshot
+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
+31 -33
View File
@@ -13,20 +13,19 @@ 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}}"
archives:
- replacements:
darwin: Darwin
linux: Linux
windows: Windows
386: 32-bit
amd64: x86_64
format_overrides:
- goos: windows
format: zip
archive:
replacements:
darwin: Darwin
linux: Linux
windows: Windows
386: 32-bit
amd64: x86_64
format_overrides:
- goos: windows
format: zip
checksum:
name_template: 'checksums.txt'
snapshot:
@@ -38,28 +37,27 @@ changelog:
- '^docs:'
- '^test:'
- '^bump'
brews:
-
# Reporitory to push the tap to.
github:
owner: jesseduffield
name: homebrew-lazygit
brew:
# Reporitory to push the tap to.
github:
owner: jesseduffield
name: homebrew-lazygit
# Your app's homepage.
# Default is empty.
homepage: 'https://github.com/jesseduffield/lazygit/'
# Your app's homepage.
# Default is empty.
homepage: 'https://github.com/jesseduffield/lazygit/'
# Your app's description.
# Default is empty.
description: 'A simple terminal UI for git commands, written in Go'
# Your app's description.
# Default is empty.
description: 'A simple terminal UI for git commands, written in Go'
# # Packages your package depends on.
# dependencies:
# - git
# - zsh
# # Packages that conflict with your package.
# conflicts:
# - svn
# - bash
# # Packages your package depends on.
# dependencies:
# - git
# - zsh
# # Packages that conflict with your package.
# conflicts:
# - svn
# - bash
# test comment to see if goreleaser only releases on new commits
# test comment to see if goreleaser only releases on new commits
+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:1.14-alpine3.11
WORKDIR /go/src/github.com/jesseduffield/lazygit/
COPY ./ .
RUN CGO_ENABLED=0 GOOS=linux go build
FROM alpine:3.11
RUN apk add -U git xdg-utils
WORKDIR /go/src/github.com/jesseduffield/lazygit/
COPY --from=0 /go/src/github.com/jesseduffield/lazygit /go/src/github.com/jesseduffield/lazygit
COPY --from=0 /go/src/github.com/jesseduffield/lazygit/lazygit /bin/
RUN echo "alias gg=lazygit" >> ~/.profile
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"
+56 -171
View File
@@ -1,78 +1,36 @@
# lazygit
![CI](https://github.com/jesseduffield/lazygit/workflows/Continuous%20Integration/badge.svg) [![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)]() [![TODOs](https://badgen.net/https/api.tickgit.com/badgen/github.com/jesseduffield/lazygit)](https://www.tickgit.com/browse?repo=github.com/jesseduffield/lazygit)
# 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.
Rant time: You've heard it before, git is _powerful_, but what good is that power when everything is so damn hard to do? Interactive rebasing requires you to edit a goddamn TODO file in your editor? *Are you kidding me?* To stage part of a file you need to use a command line program to step through each hunk and if a hunk can't be split down any further but contains code you don't want to stage, you have to edit an arcane patch file _by hand_? *Are you KIDDING me?!* Sometimes you get asked to stash your changes when switching branches only to realise that after you switch and unstash that there weren't even any conflicts and it would have been fine to just checkout the branch directly? *YOU HAVE GOT TO BE KIDDING ME!*
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!
If you're a mere mortal like me and you're tired of hearing how powerful git is when in your daily life it's a powerful pain in your ass, lazygit might be for you.
![Gif](/docs/resources/staging.gif)
![Gif](https://image.ibb.co/mmeXho/optimisedgif.gif)
## Table of contents
- [Installation](#installation)
- [Binary releases](#binary-releases)
- [Homebrew](#homebrew)
- [MacPorts](#macports)
- [Ubuntu](#ubuntu)
- [Void Linux](#void-linux)
- [Scoop (Windows)](#scoop-windows)
- [Arch Linux](#arch-linux)
- [Fedora and CentOS 7](#fedora-and-centos-7)
- [Conda](#conda)
- [Go](#go)
- [Usage](#usage)
- [Keybindings](#keybindings)
- [Changing directory on exit](#changing-directory-on-exit)
- [Undo/Redo](#undoredo)
- [Configuration](#configuration)
- [Custom pagers](#configuration)
- [Tutorials](#tutorials)
- [Cool Features](#cool-features)
- [Contributing](#contributing)
- [Donate](#donate)
- [Alternatives](#alternatives)
Github Sponsors is matching all donations dollar-for-dollar for 12 months so if you're feeling generous consider [sponsoring me](https://github.com/sponsors/jesseduffield)
[<img src="https://i.imgur.com/sVEktDn.png">](https://youtu.be/CPLdltN7wgE)
* [Installation](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
### Binary Releases
For Windows, Mac OS or Linux, you can download a binary release [here](../../releases).
### Homebrew
Normally the lazygit formula can be found in the Homebrew core but we suggest you tap our formula to get the frequently updated one. It works with Linux, too.
Tap:
```
brew install jesseduffield/lazygit/lazygit
```
Core:
```
```sh
brew tap jesseduffield/lazygit
brew install lazygit
```
### MacPorts
Latest version built from github releases.
Tap:
```
sudo port install lazygit
```
### Ubuntu
Packages for Ubuntu 16.04, 18.04 and 18.10 are available via [Launchpad PPA](https://launchpad.net/~lazygit-team).
Packages for Ubuntu are available via [Launchpad PPA](https://launchpad.net/~lazygit-team).
**Release builds**
Built from git tags. Supposed to be more stable.
```sh
sudo add-apt-repository ppa:lazygit-team/release
@@ -80,8 +38,17 @@ sudo apt-get update
sudo apt-get install lazygit
```
### Void Linux
**Daily builds**
Built from master branch once in 24 hours (or more sometimes).
```sh
sudo add-apt-repository ppa:lazygit-team/daily
sudo apt-get update
sudo apt-get install lazygit
```
### Void Linux
Packages for Void Linux are available in the distro repo
They follow upstream latest releases
@@ -90,159 +57,77 @@ They follow upstream latest releases
sudo xbps-install -S lazygit
```
### Scoop (Windows)
You can install `lazygit` using [scoop](https://scoop.sh/). It's in the `extras` bucket:
```sh
# Add the extras bucket
scoop bucket add extras
# Install lazygit
scoop install lazygit
```
### Arch Linux
Packages for Arch Linux are available via AUR (Arch User Repository).
There are two packages. The stable one which is built with the latest release
and the git version which builds from the most recent commit.
- Stable: <https://aur.archlinux.org/packages/lazygit/>
- Development: <https://aur.archlinux.org/packages/lazygit-git/>
* Stable: https://aur.archlinux.org/packages/lazygit/
* Development: https://aur.archlinux.org/packages/lazygit-git/
Instruction of how to install AUR content can be found here:
<https://wiki.archlinux.org/index.php/Arch_User_Repository>
https://wiki.archlinux.org/index.php/Arch_User_Repository
### Fedora and CentOS 7
Packages for Fedora and CentOS 7 are available via [Copr](https://copr.fedorainfracloud.org/coprs/atim/lazygit/) (Cool Other Package Repo).
```sh
sudo dnf copr enable atim/lazygit -y
sudo dnf install lazygit
```
### 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.
```sh
$ lazygit
```
If you want, you can
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).
### Keybindings
You can check out the list of keybindings [here](/docs/keybindings).
### Changing Directory On Exit
If you change repos in lazygit and want your shell to change directory into that repo on exiting lazygit, add this to your `~/.zshrc` (or other rc file):
```
lg()
{
export LAZYGIT_NEW_DIR_FILE=~/.lazygit/newdir
lazygit "$@"
if [ -f $LAZYGIT_NEW_DIR_FILE ]; then
cd "$(cat $LAZYGIT_NEW_DIR_FILE)"
rm -f $LAZYGIT_NEW_DIR_FILE > /dev/null
fi
}
```
Then `source ~/.zshrc` and from now on when you call `lg` and exit you'll switch directories to whatever you were in inside lazyigt. To override this behaviour you can exit using `shift+Q` rather than just `q`.
### Undo/Redo
See the [docs](/docs/Undoing.md)
## Configuration
Check out the [configuration docs](docs/Config.md).
### Custom Pagers
See the [docs](docs/Custom_Pagers.md)
## Tutorials
- [Video Tutorial](https://youtu.be/VDXvbHZYeKY)
- [Rebase Magic Video Tutorial](https://youtu.be/4XaToVut_hs)
- [Twitch Stream](https://www.twitch.tv/jesseduffield)
* 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/rebase.gif)
## 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/zt-5bo2clzo-hB8ZTVN5dWUCqj5QFiQVLA)
## Donate
If you would like to support the development of lazygit, consider [sponsoring me](https://github.com/sponsors/jesseduffield) (github is matching all donations dollar-for-dollar for 12 months)
[![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
+7 -256
View File
@@ -1,189 +1,26 @@
# User Config
# User Config:
Default path for the config file:
## Default:
* Linux: `~/.config/jesseduffield/lazygit/config.yml`
* MacOS: `~/Library/Application Support/jesseduffield/lazygit/config.yml`
## Default
```yaml
```
gui:
# stuff relating to the UI
scrollHeight: 2 # how many lines you scroll by
scrollPastBottom: true # enable scrolling past the bottom
sidePanelWidth: 0.3333 # number from 0 to 1
theme:
lightTheme: false # For terminals with a light background
activeBorderColor:
- white
- bold
inactiveBorderColor:
- green
- white
optionsTextColor:
- blue
selectedLineBgColor:
- blue
commitLength:
show: true
mouseEvents: true
skipUnstageLineWarning: false
skipStashWarning: true
git:
paging:
colorArg: always
useConfig: false
merging:
# only applicable to unix users
manualCommit: false
# extra args passed to `git merge`, e.g. --no-ff
args: ""
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
keybinding:
universal:
quit: 'q'
quit-alt1: '<c-c>' # alternative/alias of quit
return: '<esc>' # return to previous menu, will quit if there's nowhere to return
quitWithoutChangingDirectory: 'Q'
togglePanel: '<tab>' # goto the next panel
prevItem: '<up>' # go one line up
nextItem: '<down>' # go one line down
prevItem-alt: 'k' # go one line up
nextItem-alt: 'j' # go one line down
prevPage: ',' # go to next page in list
nextPage: '.' # go to previous page in list
gotoTop: '<' # go to top of list
gotoBottom: '>' # go to bottom of list
prevBlock: '<left>' # goto the previous block / panel
nextBlock: '<right>' # goto the next block / panel
prevBlock-alt: 'h' # goto the previous block / panel
nextBlock-alt: 'l' # goto the next block / panel
nextMatch: 'n'
prevMatch: 'N'
optionMenu: 'x' # show help menu
optionMenu-alt1: '?' # show help menu
select: '<space>'
goInto: '<enter>'
remove: 'd'
new: 'n'
edit: 'e'
openFile: 'o'
scrollUpMain: '<pgup>' # main panel scrool up
scrollDownMain: '<pgdown>' # main panel scrool down
scrollUpMain-alt1: 'K' # main panel scrool up
scrollDownMain-alt1: 'J' # main panel scrool down
scrollUpMain-alt2: '<c-u>' # main panel scrool up
scrollDownMain-alt2: '<c-d>' # main panel scrool down
executeCustomCommand: ':'
createRebaseOptionsMenu: 'm'
pushFiles: 'P'
pullFiles: 'p'
refresh: 'R'
createPatchOptionsMenu: '<c-p>'
nextTab: ']'
prevTab: '['
nextScreenMode: '+'
prevScreenMode: '_'
undo: 'z'
redo: '<c-z>'
filteringMenu: <c-s>
diffingMenu: '<c-e>'
copyToClipboard: '<c-o>'
status:
checkForUpdate: 'u'
recentRepos: '<enter>'
files:
commitChanges: 'c'
commitChangesWithoutHook: 'w' # commit changes without pre-commit hook
amendLastCommit: 'A'
commitChangesWithEditor: 'C'
ignoreFile: 'i'
refreshFiles: 'r'
stashAllChanges: 's'
viewStashOptions: 'S'
toggleStagedAll: 'a' # stage/unstage all
viewResetOptions: 'D'
fetch: 'f'
branches:
createPullRequest: 'o'
checkoutBranchByName: 'c'
forceCheckoutBranch: 'F'
rebaseBranch: 'r'
mergeIntoCurrentBranch: 'M'
viewGitFlowOptions: 'i'
fastForward: 'f' # fast-forward this branch from its upstream
pushTag: 'P'
setUpstream: 'u' # set as upstream of checked-out branch
fetchRemote: 'f'
commits:
squashDown: 's'
renameCommit: 'r'
renameCommitWithEditor: 'R'
viewResetOptions: 'g'
markCommitAsFixup: 'f'
createFixupCommit: 'F' # create fixup commit for this commit
squashAboveCommits: 'S'
moveDownCommit: '<c-j>' # move commit down one
moveUpCommit: '<c-k>' # move commit up one
amendToCommit: 'A'
pickCommit: 'p' # pick commit (when mid-rebase)
revertCommit: 't'
cherryPickCopy: 'c'
cherryPickCopyRange: 'C'
pasteCommits: 'v'
tagCommit: 'T'
checkoutCommit: '<space>'
resetCherryPick: '<c-R>'
stash:
popStash: 'g'
commitFiles:
checkoutCommitFile: 'c'
main:
toggleDragSelect: 'v'
toggleDragSelect-alt: 'V'
toggleSelectHunk: 'a'
pickBothHunks: 'b'
```
## Platform Defaults
### Windows
```yaml
# stuff relating to git
os:
openCommand: 'cmd /c "start "" {{filename}}"'
# stuff relating to the OS
```
### 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}}'
```
## Color Attributes
## Color Attributes:
For color attributes you can choose an array of attributes (with max one color attribute)
The available attributes are:
@@ -200,89 +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
selectedLineBgColor:
- blue
```
## Example Coloring
![border example](/docs/resources/colored-border-example.png)
## Keybindings
For all possible keybinding options, check [Custom_Keybindings.md](https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md)
### Example Keybindings For Colemak Users
```yaml
keybinding:
universal:
prevItem-alt: 'u'
nextItem-alt: 'e'
prevBlock-alt: 'n'
nextBlock-alt: 'i'
nextMatch: '='
prevMatch: '-'
new: 'k'
edit: 'o'
openFile: 'O'
scrollUpMain-alt1: 'U'
scrollDownMain-alt1: 'E'
scrollUpMain-alt2: '<c-u>'
scrollDownMain-alt2: '<c-e>'
files:
ignoreFile: 'I'
commits:
moveDownCommit: '<c-e>'
moveUpCommit: '<c-u>'
toggleDiffCommit: 'l'
branches:
viewGitFlowOptions: 'I'
```
## Custom pull request URLs
Some git provider setups (e.g. on-premises GitLab) can have distinct URLs for git-related calls and
the web interface/API itself. To work with those, Lazygit needs to know where it needs to create
the pull request. You can do so on your `config.yml` file using the following syntax:
```yaml
services:
"<gitDomain>": "<provider>:<webDomain>"
```
Where:
- `gitDomain` stands for the domain used by git itself (i.e. the one present on clone URLs), e.g. `git.work.com`
- `provider` is one of `github`, `bitbucket` or `gitlab`
- `webDomain` is the URL where your git service exposes a web interface and APIs, e.g. `gitservice.work.com`
## Predefined commit message prefix
In situations where certain naming pattern is used for branches and commits, pattern can be used to populate
commit message with prefix that is parsed from the branch name.
Example:
* Branch name: feature/AB-123
* Commit message: [AB-123] Adding feature
```yaml
git:
commitPrefixes:
my_project: # This is repository folder name
pattern: "^\\w+\\/(\\w+-\\w+)"
replace: "[$1] "
```
-64
View File
@@ -1,64 +0,0 @@
# Custom Pagers
Lazygit supports custom pagers, [configured](/docs/Config.md) in the config.yml file (which can be opened by pressing 'o' in the Status panel).
Support does not extend to windows users, because we're making use of a package which doesn't have windows support.
## Default:
```yaml
git:
paging:
colorArg: always
useConfig: false
```
the `colorArg` key is for whether you want the `--color=always` arg in your `git diff` command. Some pagers want it set to always, others want it set to 'never'.
## Delta:
```yaml
git:
paging:
colorArg: always
pager: delta --dark --paging=never --24-bit-color=never
```
![](https://i.imgur.com/QJpQkF3.png)
## Diff-so-fancy
```yaml
git:
paging:
colorArg: always
pager: diff-so-fancy
```
![](https://i.imgur.com/rjH1TpT.png)
## ydiff
```yaml
gui:
sidePanelWidth: 0.2 # gives you more space to show things side-by-side
git:
paging:
colorArg: never
pager: ydiff -p cat -s --wrap --width={{columnWidth}}
```
![](https://i.imgur.com/vaa8z0H.png)
Be careful with this one, I think the homebrew and pip versions are behind master. I needed to directly download the ydiff script to get the no-pager functionality working.
## Using git config
```yaml
git:
paging:
colorArg: always
useConfig: true
```
If you set `useConfig: true`, lazygit will use whatever pager is specified in $GIT_PAGER, $PAGER, or your git config. If the pager ends with something like ' | less' we will strip that part out, because less doesn't play nice with our rendering approach. If the custom pager uses less under the hood, that will also break rendering (hence the `--paging=never` flag for the `delta` pager).
+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>
-24
View File
@@ -1,24 +0,0 @@
# Undo/Redo in lazygit
![Gif](/docs/resources/undo2.gif)
## Keybindings:
'z' to undo, 'ctrl+z' to redo
## How it works
If you're as clumsy as me you'll probably have felt the pain of botching an interactive rebase or doing a hard reset onto the wrong commit. Luckily, the reflog allows you to trace your steps and make things right again, but I personally can't stand trying to make sense of the reflog.
Lazygit can read through your reflog for you and walk back action by action so that you don't even need to read the reflog. If lazygit finds a reflog entry where you checked out a branch, we'll checkout the original branch. If the entry is from a commit being applied, we'll go back to the commit before that. If we hit an interactive rebase, we'll go back to the commit you were on just before you started it.
## You can even undo things you did outside of lazygit!
Because lazygit just uses the reflog to keep track of things, it doesn't matter whether you're trying to undo something you did in lazygit or directly on the command line. You can open lazygit for the first time and start undoing thing in your repo! Likewise, lazygit marks its undos/redos in the reflog so if you quit the application and come back, lazygit still knows where you're up to.
## Limitations
There are limitations: firstly, lazygit can only undo things that are recorded in the reflog. That means changes to your working tree or stash aren't covered. Secondly, anything permanent you do like pushing to a remote can't be undone. Thirdly, actions like creating a branch won't be undone, because they're not stored in the reflog.
If you are mid-rebase, undo/redo is not supported, because the reflog doesn't enough contain information about what specific things have happened inside that rebase. If you want to undo out of a rebase, it's best to abort the rebase (the default keybinding for bringing up rebase options is 'm').
Undo/Redo is a new feature so if you find a bug let us know. The worst case scenario is that you'll just need to look at your reflog and manually put yourself back on track.
-59
View File
@@ -1,59 +0,0 @@
## Possible keybindings
| Put in | You will get |
|---------------|----------------|
| `<f1>` | F1 |
| `<f2>` | F2 |
| `<f3>` | F3 |
| `<f4>` | F4 |
| `<f5>` | F5 |
| `<f6>` | F6 |
| `<f7>` | F7 |
| `<f8>` | F8 |
| `<f9>` | F9 |
| `<f10>` | F10 |
| `<f11>` | F11 |
| `<f12>` | F12 |
| `<insert>` | Insert |
| `<delete>` | Delete |
| `<home>` | Home |
| `<end>` | End |
| `<pgup>` | Pgup |
| `<pgdown>` | Pgdn |
| `<up>` | ArrowUp |
| `<down>` | ArrowDown |
| `<left>` | ArrowLeft |
| `<right>` | ArrowRight |
| `<tab>` | Tab |
| `<enter>` | Enter |
| `<esc>` | Esc |
| `<backspace>` | Backspace |
| `<c-space>` | CtrlSpace |
| `<c-/>` | CtrlSlash |
| `<space>` | Space |
| `<c-a>` | CtrlA |
| `<c-b>` | CtrlB |
| `<c-c>` | CtrlC |
| `<c-d>` | CtrlD |
| `<c-e>` | CtrlE |
| `<c-f>` | CtrlF |
| `<c-g>` | CtrlG |
| `<c-j>` | CtrlJ |
| `<c-k>` | CtrlK |
| `<c-l>` | CtrlL |
| `<c-n>` | CtrlN |
| `<c-o>` | CtrlO |
| `<c-p>` | CtrlP |
| `<c-q>` | CtrlQ |
| `<c-r>` | CtrlR |
| `<c-s>` | CtrlS |
| `<c-t>` | CtrlT |
| `<c-u>` | CtrlU |
| `<c-v>` | CtrlV |
| `<c-w>` | CtrlW |
| `<c-x>` | CtrlX |
| `<c-y>` | CtrlY |
| `<c-z>` | CtrlZ |
| `<c-4>` | Ctrl4 |
| `<c-5>` | Ctrl5 |
| `<c-6>` | Ctrl6 |
| `<c-8>` | Ctrl8 |
-277
View File
@@ -1,277 +0,0 @@
# Lazygit Keybindings
## Global Keybindings
<pre>
<kbd>pgup</kbd>: scroll up main panel (fn+up)
<kbd>pgdown</kbd>: scroll down main panel (fn+down)
<kbd>m</kbd>: view merge/rebase options
<kbd>ctrl+p</kbd>: view custom patch options
<kbd>P</kbd>: push
<kbd>p</kbd>: pull
<kbd>R</kbd>: refresh
<kbd>x</kbd>: open menu
<kbd>z</kbd>: undo (via reflog) (experimental)
<kbd>ctrl+z</kbd>: redo (via reflog) (experimental)
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode
<kbd>:</kbd>: execute custom command
</pre>
## Branches Panel
<pre>
<kbd>]</kbd>: next tab
<kbd>[</kbd>: previous tab
</pre>
## Branches Panel (Branches Tab)
<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 checked-out branch onto this branch
<kbd>M</kbd>: merge into currently checked out branch
<kbd>i</kbd>: show git-flow options
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>g</kbd>: view reset options
<kbd>R</kbd>: rename branch
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Branches Panel (Remote Branches (in Remotes tab))
<pre>
<kbd>esc</kbd>: return to remotes list
<kbd>g</kbd>: view reset options
<kbd>space</kbd>: checkout
<kbd>M</kbd>: merge into currently checked out branch
<kbd>d</kbd>: delete branch
<kbd>r</kbd>: rebase checked-out branch onto this branch
<kbd>u</kbd>: set as upstream of checked-out branch
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Branches Panel (Remotes Tab)
<pre>
<kbd>f</kbd>: fetch remote
<kbd>n</kbd>: add new remote
<kbd>d</kbd>: remove remote
<kbd>e</kbd>: edit remote
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Branches Panel (Tags Tab)
<pre>
<kbd>space</kbd>: checkout
<kbd>d</kbd>: delete tag
<kbd>P</kbd>: push tag
<kbd>n</kbd>: create tag
<kbd>g</kbd>: view reset options
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Commit Files Panel
<pre>
<kbd>esc</kbd>: go back
<kbd>c</kbd>: checkout file
<kbd>d</kbd>: discard this commit's changes to this file
<kbd>o</kbd>: open file
<kbd>space</kbd>: toggle file included in patch
<kbd>enter</kbd>: enter file to add selected lines to the patch
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Commits Panel
<pre>
<kbd>]</kbd>: next tab
<kbd>[</kbd>: previous tab
</pre>
## Commits Panel (Commits Tab)
<pre>
<kbd>s</kbd>: squash down
<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>: checkout commit
<kbd>i</kbd>: select commit to diff with another commit
<kbd>T</kbd>: tag commit
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Commits Panel (Reflog Tab)
<pre>
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: view reset options
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Files Panel
<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 changes
<kbd>S</kbd>: view stash options
<kbd>a</kbd>: stage/unstage all
<kbd>D</kbd>: view reset options
<kbd>enter</kbd>: stage individual hunks/lines
<kbd>f</kbd>: fetch
<kbd>g</kbd>: view upstream reset options
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Main Panel (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>
## Main Panel (Normal)
<pre>
<kbd> ̄</kbd>: scroll down (fn+up)
<kbd>¦</kbd>: scroll up (fn+down)
</pre>
## Main Panel (Patch Building)
<pre>
<kbd>esc</kbd>: exit line-by-line mode
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>space</kbd>: add/remove line(s) to patch
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
</pre>
## Main Panel (Staging)
<pre>
<kbd>esc</kbd>: return to files panel
<kbd>space</kbd>: toggle line staged / unstaged
<kbd>d</kbd>: delete change (git reset)
<kbd>tab</kbd>: switch to other panel
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>e</kbd>: edit file
<kbd>o</kbd>: open file
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
<kbd>c</kbd>: commit changes
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>C</kbd>: commit changes using git editor
</pre>
## Menu Panel
<pre>
<kbd>esc</kbd>: close menu
<kbd>q</kbd>: close menu
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Stash Panel
<pre>
<kbd>space</kbd>: apply
<kbd>g</kbd>: pop
<kbd>d</kbd>: drop
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Status Panel
<pre>
<kbd>e</kbd>: edit config file
<kbd>o</kbd>: open config file
<kbd>u</kbd>: check for update
<kbd>enter</kbd>: switch to a recent repo
</pre>
-277
View File
@@ -1,277 +0,0 @@
# Lazygit Keybindings
## Global
<pre>
<kbd>pgup</kbd>: scroll up main panel (fn+up)
<kbd>pgdown</kbd>: scroll down main panel (fn+down)
<kbd>m</kbd>: bekijk merge/rebase opties
<kbd>ctrl+p</kbd>: view custom patch options
<kbd>P</kbd>: push
<kbd>p</kbd>: pull
<kbd>R</kbd>: verversen
<kbd>x</kbd>: open menu
<kbd>z</kbd>: undo (via reflog) (experimental)
<kbd>ctrl+z</kbd>: redo (via reflog) (experimental)
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode
<kbd>:</kbd>: voor aangepast commando uit
</pre>
## Branches Panel
<pre>
<kbd>]</kbd>: next tab
<kbd>[</kbd>: previous tab
</pre>
## Branches Panel (Branches Tab)
<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>i</kbd>: show git-flow options
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>g</kbd>: bekijk reset opties
<kbd>R</kbd>: rename branch
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Branches Panel (Remote Branches (in Remotes tab))
<pre>
<kbd>esc</kbd>: return to remotes list
<kbd>g</kbd>: bekijk reset opties
<kbd>space</kbd>: uitchecken
<kbd>M</kbd>: merge in met huidige checked out branch
<kbd>d</kbd>: verwijder branch
<kbd>r</kbd>: rebase branch
<kbd>u</kbd>: set as upstream of checked-out branch
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Branches Panel (Remotes Tab)
<pre>
<kbd>f</kbd>: fetch remote
<kbd>n</kbd>: add new remote
<kbd>d</kbd>: remove remote
<kbd>e</kbd>: edit remote
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Branches Panel (Tags Tab)
<pre>
<kbd>space</kbd>: uitchecken
<kbd>d</kbd>: delete tag
<kbd>P</kbd>: push tag
<kbd>n</kbd>: create tag
<kbd>g</kbd>: bekijk reset opties
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Commit bestanden Panel
<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
<kbd>space</kbd>: toggle file included in patch
<kbd>enter</kbd>: enter file to add selected lines to the patch
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Commits Panel
<pre>
<kbd>]</kbd>: next tab
<kbd>[</kbd>: previous tab
</pre>
## Commits Panel (Commits Tab)
<pre>
<kbd>s</kbd>: squash 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>: checkout commit
<kbd>i</kbd>: select commit to diff with another commit
<kbd>T</kbd>: tag commit
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Commits Panel (Reflog Tab)
<pre>
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: bekijk reset opties
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Bestanden Panel
<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>S</kbd>: view stash options
<kbd>a</kbd>: toggle staged alle
<kbd>D</kbd>: bekijk reset opties
<kbd>enter</kbd>: stage individuele hunks/lijnen
<kbd>f</kbd>: fetch
<kbd>g</kbd>: view upstream reset options
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Hoofd Panel (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 Panel (Normaal)
<pre>
<kbd> ̄</kbd>: scroll omlaag (fn+up)
<kbd>¦</kbd>: scroll omhoog (fn+down)
</pre>
## Hoofd Panel (Patch Building)
<pre>
<kbd>esc</kbd>: exit line-by-line mode
<kbd>▲</kbd>: selecteer de vorige lijn
<kbd>▼</kbd>: selecteer de volgende lijn
<kbd>◄</kbd>: selecteer de vorige hunk
<kbd>►</kbd>: selecteer de volgende hunk
<kbd>space</kbd>: add/remove line(s) to patch
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
</pre>
## Hoofd Panel (Stage Lines/Hunks)
<pre>
<kbd>esc</kbd>: ga terug naar het bestanden paneel
<kbd>space</kbd>: toggle line staged / unstaged
<kbd>d</kbd>: delete change (git reset)
<kbd>tab</kbd>: switch to other panel
<kbd>▲</kbd>: selecteer de vorige lijn
<kbd>▼</kbd>: selecteer de volgende lijn
<kbd>◄</kbd>: selecteer de vorige hunk
<kbd>►</kbd>: selecteer de volgende hunk
<kbd>e</kbd>: verander bestand
<kbd>o</kbd>: open bestand
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
<kbd>c</kbd>: Commit veranderingen
<kbd>w</kbd>: commit veranderingen zonder pre-commit hook
<kbd>C</kbd>: commit veranderingen met de git editor
</pre>
## Menu Panel
<pre>
<kbd>esc</kbd>: close menu
<kbd>q</kbd>: close menu
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Stash Panel
<pre>
<kbd>space</kbd>: toepassen
<kbd>g</kbd>: pop
<kbd>d</kbd>: drop
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Status Panel
<pre>
<kbd>e</kbd>: verander config file
<kbd>o</kbd>: open config file
<kbd>u</kbd>: check voor updates
<kbd>enter</kbd>: wissel naar een recente repo
</pre>
-277
View File
@@ -1,277 +0,0 @@
# Lazygit Keybindings
## Globalne
<pre>
<kbd>pgup</kbd>: scroll up main panel (fn+up)
<kbd>pgdown</kbd>: scroll down main panel (fn+down)
<kbd>m</kbd>: view merge/rebase options
<kbd>ctrl+p</kbd>: view custom patch options
<kbd>P</kbd>: push
<kbd>p</kbd>: pull
<kbd>R</kbd>: odśwież
<kbd>x</kbd>: open menu
<kbd>z</kbd>: undo (via reflog) (experimental)
<kbd>ctrl+z</kbd>: redo (via reflog) (experimental)
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode
<kbd>:</kbd>: execute custom command
</pre>
## Gałęzie Panel
<pre>
<kbd>]</kbd>: next tab
<kbd>[</kbd>: previous tab
</pre>
## Gałęzie Panel (Branches Tab)
<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>i</kbd>: show git-flow options
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>g</kbd>: view reset options
<kbd>R</kbd>: rename branch
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Gałęzie Panel (Remote Branches (in Remotes tab))
<pre>
<kbd>esc</kbd>: return to remotes list
<kbd>g</kbd>: view reset options
<kbd>space</kbd>: przełącz
<kbd>M</kbd>: scal do obecnej gałęzi
<kbd>d</kbd>: usuń gałąź
<kbd>r</kbd>: rebase branch
<kbd>u</kbd>: set as upstream of checked-out branch
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Gałęzie Panel (Remotes Tab)
<pre>
<kbd>f</kbd>: fetch remote
<kbd>n</kbd>: add new remote
<kbd>d</kbd>: remove remote
<kbd>e</kbd>: edit remote
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Gałęzie Panel (Tags Tab)
<pre>
<kbd>space</kbd>: przełącz
<kbd>d</kbd>: delete tag
<kbd>P</kbd>: push tag
<kbd>n</kbd>: create tag
<kbd>g</kbd>: view reset options
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Commit files Panel
<pre>
<kbd>esc</kbd>: go back
<kbd>c</kbd>: checkout file
<kbd>d</kbd>: discard this commit's changes to this file
<kbd>o</kbd>: otwórz plik
<kbd>space</kbd>: toggle file included in patch
<kbd>enter</kbd>: enter file to add selected lines to the patch
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Commity Panel
<pre>
<kbd>]</kbd>: next tab
<kbd>[</kbd>: previous tab
</pre>
## Commity Panel (Commits Tab)
<pre>
<kbd>s</kbd>: ściśnij w dół
<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>: checkout commit
<kbd>i</kbd>: select commit to diff with another commit
<kbd>T</kbd>: tag commit
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Commity Panel (Reflog Tab)
<pre>
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: view reset options
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Pliki Panel
<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>S</kbd>: view stash options
<kbd>a</kbd>: przełącz wszystkie zatwierdzenia
<kbd>D</kbd>: view reset options
<kbd>enter</kbd>: zatwierdź pojedyncze linie
<kbd>f</kbd>: fetch
<kbd>g</kbd>: view upstream reset options
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Main Panel (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>: cofnij
</pre>
## Main Panel (Normal)
<pre>
<kbd> ̄</kbd>: scroll down (fn+up)
<kbd>¦</kbd>: scroll up (fn+down)
</pre>
## Main Panel (Patch Building)
<pre>
<kbd>esc</kbd>: exit line-by-line mode
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>space</kbd>: add/remove line(s) to patch
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
</pre>
## Main Panel (Zatwierdzanie)
<pre>
<kbd>esc</kbd>: wróć do panelu plików
<kbd>space</kbd>: toggle line staged / unstaged
<kbd>d</kbd>: delete change (git reset)
<kbd>tab</kbd>: switch to other panel
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>e</kbd>: edytuj plik
<kbd>o</kbd>: otwórz plik
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
<kbd>c</kbd>: commituj zmiany
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>C</kbd>: commituj zmiany używając edytora z gita
</pre>
## Menu Panel
<pre>
<kbd>esc</kbd>: close menu
<kbd>q</kbd>: close menu
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Schowek Panel
<pre>
<kbd>space</kbd>: zastosuj
<kbd>g</kbd>: wyciągnij
<kbd>d</kbd>: porzuć
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
</pre>
## Status Panel
<pre>
<kbd>e</kbd>: edytuj plik konfiguracyjny
<kbd>o</kbd>: otwórz plik konfiguracyjny
<kbd>u</kbd>: sprawdź aktualizacje
<kbd>enter</kbd>: switch to a recent repo
</pre>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 650 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 539 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

-39
View File
@@ -1,39 +0,0 @@
module github.com/jesseduffield/lazygit
go 1.14
require (
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
github.com/creack/pty v1.1.10-0.20191209115840-8ab47f72e854
github.com/fatih/color v1.7.0
github.com/fsnotify/fsnotify v1.4.7
github.com/go-errors/errors v1.0.1
github.com/go-git/go-git/v5 v5.0.0
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
github.com/golang/protobuf v1.3.2 // indirect
github.com/google/go-cmp v0.3.1 // indirect
github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/gocui v0.3.1-0.20200309001002-7765949e1c8a
github.com/jesseduffield/termbox-go v0.0.0-20200130214842-1d31d1faa3c9 // indirect
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/mattn/go-colorable v0.1.4 // indirect
github.com/mattn/go-isatty v0.0.11 // indirect
github.com/mattn/go-runewidth v0.0.8
github.com/mgutz/str v1.2.0
github.com/nicksnyder/go-i18n/v2 v2.0.3
github.com/onsi/ginkgo v1.10.3 // indirect
github.com/onsi/gomega v1.7.1 // indirect
github.com/pelletier/go-toml v1.6.0 // indirect
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0
github.com/sirupsen/logrus v1.4.2
github.com/spf13/afero v1.2.2 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.6.1
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
github.com/stretchr/testify v1.4.0
github.com/tcnksm/go-gitconfig v0.1.2
golang.org/x/text v0.3.2
gopkg.in/yaml.v2 v2.2.7
)
-280
View File
@@ -1,280 +0,0 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.10-0.20191209115840-8ab47f72e854 h1:NB4neYMzyBsw52kUdkTrQm4Q05ErObCdwLvJptpfJSc=
github.com/creack/pty v1.1.10-0.20191209115840-8ab47f72e854/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM=
github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-git-fixtures/v4 v4.0.1 h1:q+IFMfLx200Q3scvt2hN79JsEzy4AmBTp/pqnefH+Bc=
github.com/go-git/go-git-fixtures/v4 v4.0.1/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
github.com/go-git/go-git/v5 v5.0.0 h1:k5RWPm4iJwYtfWoxIJy4wJX9ON7ihPeZZYC1fLYDnpg=
github.com/go-git/go-git/v5 v5.0.0/go.mod h1:oYD8y9kWsGINPFJoLdaScGCN6dlKg23blmClfZwtUVA=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 h1:zN2lZNZRflqFyxVaTIU61KNKQ9C0055u9CAfpmqUvo4=
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3/go.mod h1:nPpo7qLxd6XL3hWJG/O60sR8ZKfMCiIoNap5GvD12KU=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/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/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/integrii/flaggy v1.4.0 h1:A1x7SYx4jqu5NSrY14z8Z+0UyX2S5ygfJJrfolWR3zM=
github.com/integrii/flaggy v1.4.0/go.mod h1:tnTxHeTJbah0gQ6/K0RW0J7fMUBk9MCF5blhm43LNpI=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jesseduffield/gocui v0.3.1-0.20200309001002-7765949e1c8a h1:JSORQue6V4bMppr22dtUuYX+w79cgupo66PcGZ9ijlU=
github.com/jesseduffield/gocui v0.3.1-0.20200309001002-7765949e1c8a/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/termbox-go v0.0.0-20200130214842-1d31d1faa3c9 h1:iBBk1lhFwjwJw//J2m1yyz9S368GeXQTpMVACTyQMh0=
github.com/jesseduffield/termbox-go v0.0.0-20200130214842-1d31d1faa3c9/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0=
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mgutz/str v1.2.0 h1:4IzWSdIz9qPQWLfKZ0rJcV0jcUDpxvP4JVZ4GXQyvSw=
github.com/mgutz/str v1.2.0/go.mod h1:w1v0ofgLaJdoD0HpQ3fycxKD1WtxpjSo151pK/31q6w=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nicksnyder/go-i18n/v2 v2.0.3 h1:ks/JkQiOEhhuF6jpNvx+Wih1NIiXzUnZeZVnJuI8R8M=
github.com/nicksnyder/go-i18n/v2 v2.0.3/go.mod h1:oDab7q8XCYMRlcrBnaY/7B1eOectbvj6B1UPBT+p5jo=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY=
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 h1:Xuk8ma/ibJ1fOy4Ee11vHhUFHQNpHhrBneOCNHVXS5w=
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0/go.mod h1:7AwjWCpdPhkSmNAgUv5C7EJ4AbmjEB3r047r3DXWu3Y=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk=
github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/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.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+40 -62
View File
@@ -1,83 +1,61 @@
package main
import (
"flag"
"fmt"
"log"
"io/ioutil"
"os"
"runtime"
"path/filepath"
"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 {
gopath := os.Getenv("GOPATH")
return filepath.FromSlash(gopath + "/src/github.com/jesseduffield/lazygit/" + path)
}
// 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 {
return "unversioned"
}
return string(byteVersion)
}
func main() {
flaggy.DefaultParser.ShowVersionWithVersionFlag = false
repoPath := "."
flaggy.String(&repoPath, "p", "path", "Path of git repo")
filterPath := ""
flaggy.String(&filterPath, "f", "filter", "Path to filter on in `git log -- <path>`. When in filter mode, the commits, reflog, and stash are filtered based on the given path, and some operations are restricted")
dump := ""
flaggy.AddPositionalValue(&dump, "gitargs", 1, false, "Todo file")
flaggy.DefaultParser.PositionalFlags[0].Hidden = true
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)
flag.Parse()
if version == "unversioned" {
version = fallbackVersion()
}
if configFlag {
fmt.Printf("%s\n", config.GetDefaultConfig())
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)
}
if repoPath != "." {
if err := os.Chdir(repoPath); err != nil {
log.Fatal(err.Error())
}
}
appConfig, err := config.NewAppConfig("lazygit", version, commit, date, buildSource, debuggingFlag)
appConfig, err := config.NewAppConfig("lazygit", version, commit, date, debuggingFlag)
if err != nil {
log.Fatal(err.Error())
panic(err)
}
app, err := app.NewApp(appConfig, filterPath)
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, err := app.NewApp(appConfig)
app.Log.Info(err)
app.GitCommand.SetupGit()
app.Gui.RunWithSubprocesses()
}
+23 -155
View File
@@ -1,68 +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/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)
}
@@ -70,116 +38,35 @@ func newDevelopmentLogger(config config.AppConfigurer) *logrus.Logger {
return log
}
func newLogger(config config.AppConfigurer) *logrus.Entry {
var log *logrus.Logger
if config.GetDebug() || os.Getenv("DEBUG") == "TRUE" {
log = newDevelopmentLogger(config)
} else {
log = newProductionLogger(config)
}
// highly recommended: tail -f development.log | humanlog
// https://github.com/aybabtme/humanlog
log.Formatter = &logrus.JSONFormatter{}
return log.WithFields(logrus.Fields{
"debug": config.GetDebug(),
"version": config.GetVersion(),
"commit": config.GetCommit(),
"buildDate": config.GetBuildDate(),
})
}
// NewApp bootstrap a new application
func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
// NewApp retruns a new applications
func NewApp(config config.AppConfigurer) (*App, error) {
app := &App{
closers: []io.Closer{},
Config: config,
}
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, filterPath)
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 {
@@ -190,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
}
+33 -8
View File
@@ -1,14 +1,39 @@
package commands
import (
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// Branch : A git branch
// duplicating this for now
type Branch struct {
Name string
// the displayname is something like '(HEAD detached at 123asdf)', whereas in that case the name would be '123asdf'
DisplayName string
Recency string
Pushables string
Pullables string
UpstreamName string
Head bool
Name string
Recency string
}
// 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
func (b *Branch) GetColor() color.Attribute {
switch b.getType() {
case "feature":
return color.FgGreen
case "bugfix":
return color.FgYellow
case "hotfix":
return color.FgRed
default:
return color.FgWhite
}
}
// expected to return feature/bugfix/hotfix or blank string
func (b *Branch) getType() string {
return strings.Split(b.Name, "/")[0]
}
-160
View File
@@ -1,160 +0,0 @@
package commands
import (
"regexp"
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
// 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
// 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
GitCommand *GitCommand
ReflogCommits []*Commit
}
// NewBranchListBuilder builds a new branch list builder
func NewBranchListBuilder(log *logrus.Entry, gitCommand *GitCommand, reflogCommits []*Commit) (*BranchListBuilder, error) {
return &BranchListBuilder{
Log: log,
GitCommand: gitCommand,
ReflogCommits: reflogCommits,
}, nil
}
func (b *BranchListBuilder) obtainBranches() []*Branch {
cmdStr := `git for-each-ref --sort=-committerdate --format="%(HEAD)|%(refname:short)|%(upstream:short)|%(upstream:track)" refs/heads`
output, err := b.GitCommand.OSCommand.RunCommandWithOutput(cmdStr)
if err != nil {
panic(err)
}
trimmedOutput := strings.TrimSpace(output)
outputLines := strings.Split(trimmedOutput, "\n")
branches := make([]*Branch, 0, len(outputLines))
for _, line := range outputLines {
if line == "" {
continue
}
split := strings.Split(line, SEPARATION_CHAR)
name := split[1]
branch := &Branch{
Name: name,
Pullables: "?",
Pushables: "?",
Head: split[0] == "*",
}
upstreamName := split[2]
if upstreamName == "" {
branches = append(branches, branch)
continue
}
branch.UpstreamName = upstreamName
track := split[3]
re := regexp.MustCompile(`ahead (\d+)`)
match := re.FindStringSubmatch(track)
if len(match) > 1 {
branch.Pushables = match[1]
} else {
branch.Pushables = "0"
}
re = regexp.MustCompile(`behind (\d+)`)
match = re.FindStringSubmatch(track)
if len(match) > 1 {
branch.Pullables = match[1]
} else {
branch.Pullables = "0"
}
branches = append(branches, branch)
}
return branches
}
// Build the list of branches for the current repo
func (b *BranchListBuilder) Build() []*Branch {
branches := b.obtainBranches()
reflogBranches := b.obtainReflogBranches()
// loop through reflog branches. If there is a match, merge them, then remove it from the branches and keep it in the reflog branches
branchesWithRecency := make([]*Branch, 0)
outer:
for _, reflogBranch := range reflogBranches {
for j, branch := range branches {
if branch.Head {
continue
}
if strings.EqualFold(reflogBranch.Name, branch.Name) {
branch.Recency = reflogBranch.Recency
branchesWithRecency = append(branchesWithRecency, branch)
branches = append(branches[0:j], branches[j+1:]...)
continue outer
}
}
}
branches = append(branchesWithRecency, branches...)
foundHead := false
for i, branch := range branches {
if branch.Head {
foundHead = true
branch.Recency = " *"
branches = append(branches[0:i], branches[i+1:]...)
branches = append([]*Branch{branch}, branches...)
break
}
}
if !foundHead {
currentBranchName, currentBranchDisplayName, err := b.GitCommand.CurrentBranchName()
if err != nil {
panic(err)
}
branches = append([]*Branch{{Name: currentBranchName, DisplayName: currentBranchDisplayName, Head: true, Recency: " *"}}, branches...)
}
return branches
}
// TODO: only look at the new reflog commits, and otherwise store the recencies in
// int form against the branch to recalculate the time ago
func (b *BranchListBuilder) obtainReflogBranches() []*Branch {
foundBranchesMap := map[string]bool{}
re := regexp.MustCompile(`checkout: moving from ([\S]+) to ([\S]+)`)
reflogBranches := make([]*Branch, 0, len(b.ReflogCommits))
for _, commit := range b.ReflogCommits {
if match := re.FindStringSubmatch(commit.Name); len(match) == 3 {
recency := utils.UnixToTimeAgo(commit.UnixTimestamp)
for _, branchName := range match[1:] {
if !foundBranchesMap[branchName] {
foundBranchesMap[branchName] = true
reflogBranches = append(reflogBranches, &Branch{
Recency: recency,
Name: branchName,
})
}
}
}
}
return reflogBranches
}
-20
View File
@@ -1,20 +0,0 @@
package commands
// Commit : A git commit
type Commit struct {
Sha string
Name string
Status string // one of "unpushed", "pushed", "merged", "rebasing" or "selected"
Action string // one of "", "pick", "edit", "squash", "reword", "drop", "fixup"
Tags []string
ExtraInfo string // something like 'HEAD -> master, tag: v0.15.2'
Author string
UnixTimestamp int64
}
func (c *Commit) ShortSha() string {
if len(c.Sha) < 8 {
return c.Sha
}
return c.Sha[:8]
}
-19
View File
@@ -1,19 +0,0 @@
package commands
// CommitFile : A git commit file
type CommitFile struct {
Sha string
Name string
DisplayString string
Status int // one of 'WHOLE' 'PART' 'NONE'
}
const (
// UNSELECTED is for when the commit file has not been added to the patch in any way
UNSELECTED = iota
// WHOLE is for when you want to add the whole diff of a file to the patch,
// including e.g. if it was deleted
WHOLE = iota
// PART is for when you're only talking about specific lines that have been modified
PART
)
-313
View File
@@ -1,313 +0,0 @@
package commands
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/fatih/color"
"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
const SEPARATION_CHAR = "|"
// CommitListBuilder returns a list of Branch objects for the current repo
type CommitListBuilder struct {
Log *logrus.Entry
GitCommand *GitCommand
OSCommand *OSCommand
Tr *i18n.Localizer
CherryPickedCommits []*Commit
}
// NewCommitListBuilder builds a new commit list builder
func NewCommitListBuilder(log *logrus.Entry, gitCommand *GitCommand, osCommand *OSCommand, tr *i18n.Localizer, cherryPickedCommits []*Commit) (*CommitListBuilder, error) {
return &CommitListBuilder{
Log: log,
GitCommand: gitCommand,
OSCommand: osCommand,
Tr: tr,
CherryPickedCommits: cherryPickedCommits,
}, nil
}
// extractCommitFromLine takes a line from a git log and extracts the sha, message, date, and tag if present
// then puts them into a commit object
// example input:
// 8ad01fe32fcc20f07bc6693f87aa4977c327f1e1|10 hours ago|Jesse Duffield| (HEAD -> master, tag: v0.15.2)|refresh commits when adding a tag
func (c *CommitListBuilder) extractCommitFromLine(line string) *Commit {
split := strings.Split(line, SEPARATION_CHAR)
sha := split[0]
unixTimestamp := split[1]
author := split[2]
extraInfo := strings.TrimSpace(split[3])
message := strings.Join(split[4:], SEPARATION_CHAR)
tags := []string{}
if extraInfo != "" {
re := regexp.MustCompile(`tag: ([^,\)]+)`)
tagMatch := re.FindStringSubmatch(extraInfo)
if len(tagMatch) > 1 {
tags = append(tags, tagMatch[1])
}
}
unitTimestampInt, _ := strconv.Atoi(unixTimestamp)
return &Commit{
Sha: sha,
Name: message,
Tags: tags,
ExtraInfo: extraInfo,
UnixTimestamp: int64(unitTimestampInt),
Author: author,
}
}
type GetCommitsOptions struct {
Limit bool
FilterPath string
}
// GetCommits obtains the commits of the current branch
func (c *CommitListBuilder) GetCommits(options GetCommitsOptions) ([]*Commit, error) {
commits := []*Commit{}
var rebasingCommits []*Commit
rebaseMode, err := c.GitCommand.RebaseMode()
if err != nil {
return nil, err
}
if rebaseMode != "" && options.FilterPath == "" {
// here we want to also prepend the commits that we're in the process of rebasing
rebasingCommits, err = c.getRebasingCommits(rebaseMode)
if err != nil {
return nil, err
}
if len(rebasingCommits) > 0 {
commits = append(commits, rebasingCommits...)
}
}
unpushedCommits := c.getUnpushedCommits()
cmd := c.getLogCmd(options)
err = RunLineOutputCmd(cmd, func(line string) (bool, error) {
if strings.Split(line, " ")[0] != "gpg:" {
commit := c.extractCommitFromLine(line)
_, unpushed := unpushedCommits[commit.ShortSha()]
commit.Status = map[bool]string{true: "unpushed", false: "pushed"}[unpushed]
commits = append(commits, commit)
}
return false, nil
})
if err != nil {
return nil, err
}
if rebaseMode != "" {
currentCommit := commits[len(rebasingCommits)]
blue := color.New(color.FgYellow)
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
}
return commits, nil
}
// getRebasingCommits obtains the commits that we're in the process of rebasing
func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*Commit, error) {
switch rebaseMode {
case "normal":
return c.getNormalRebasingCommits()
case "interactive":
return c.getInteractiveRebasingCommits()
default:
return nil, nil
}
}
func (c *CommitListBuilder) getNormalRebasingCommits() ([]*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 := []*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([]*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() ([]*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 := []*Commit{}
lines := strings.Split(string(bytesContent), "\n")
for _, line := range lines {
if line == "" || line == "noop" {
return commits, nil
}
splitLine := strings.Split(line, " ")
commits = append([]*Commit{{
Sha: splitLine[1],
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) (*Commit, error) {
lines := strings.Split(content, "\n")
sha := strings.Split(lines[0], " ")[1]
name := strings.TrimPrefix(lines[3], "Subject: ")
return &Commit{
Sha: sha,
Name: name,
Status: "rebasing",
}, nil
}
func (c *CommitListBuilder) setCommitMergedStatuses(commits []*Commit) ([]*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) 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("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 --abbrev=8")
if err != nil {
return pushables
}
for _, p := range utils.SplitLines(o) {
pushables[p] = true
}
return pushables
}
// getLog gets the git log.
func (c *CommitListBuilder) getLogCmd(options GetCommitsOptions) *exec.Cmd {
limitFlag := ""
if options.Limit {
limitFlag = "-300"
}
filterFlag := ""
if options.FilterPath != "" {
filterFlag = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(options.FilterPath))
}
return c.OSCommand.ExecutableFromString(fmt.Sprintf("git log --oneline --pretty=format:\"%%H%s%%at%s%%aN%s%%d%s%%s\" %s --abbrev=%d --date=unix %s", SEPARATION_CHAR, SEPARATION_CHAR, SEPARATION_CHAR, SEPARATION_CHAR, limitFlag, 20, filterFlag))
}
-151
View File
@@ -1,151 +0,0 @@
package commands
import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/stretchr/testify/assert"
)
// NewDummyCommitListBuilder creates a new dummy CommitListBuilder for testing
func NewDummyCommitListBuilder() *CommitListBuilder {
osCommand := NewDummyOSCommand()
return &CommitListBuilder{
Log: NewDummyLog(),
GitCommand: NewDummyGitCommandWithOSCommand(osCommand),
OSCommand: osCommand,
Tr: i18n.NewLocalizer(NewDummyLog()),
CherryPickedCommits: []*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())
})
}
}
-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/creack/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)
}
-16
View File
@@ -1,16 +0,0 @@
package commands
// 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 ', '??'
}
+297 -1052
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
-58
View File
@@ -1,58 +0,0 @@
package commands
import (
"fmt"
"regexp"
"sort"
"strings"
)
func (c *GitCommand) GetRemotes() ([]*Remote, error) {
// get remote branches
unescaped := "git branch -r"
remoteBranchesStr, err := c.OSCommand.RunCommandWithOutput(unescaped)
if err != nil {
return nil, err
}
goGitRemotes, err := c.Repo.Remotes()
if err != nil {
return nil, err
}
// first step is to get our remotes from go-git
remotes := make([]*Remote, len(goGitRemotes))
for i, goGitRemote := range goGitRemotes {
remoteName := goGitRemote.Config().Name
re := regexp.MustCompile(fmt.Sprintf(`%s\/([\S]+)`, remoteName))
matches := re.FindAllStringSubmatch(remoteBranchesStr, -1)
branches := make([]*RemoteBranch, len(matches))
for j, match := range matches {
branches[j] = &RemoteBranch{
Name: match[1],
RemoteName: remoteName,
}
}
remotes[i] = &Remote{
Name: goGitRemote.Config().Name,
Urls: goGitRemote.Config().URLs,
Branches: branches,
}
}
// now lets sort our remotes by name alphabetically
sort.Slice(remotes, func(i, j int) bool {
// we want origin at the top because we'll be most likely to want it
if remotes[i].Name == "origin" {
return true
}
if remotes[j].Name == "origin" {
return false
}
return strings.ToLower(remotes[i].Name) < strings.ToLower(remotes[j].Name)
})
return remotes, nil
}
-87
View File
@@ -1,87 +0,0 @@
package commands
import (
"regexp"
"sort"
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
)
const semverRegex = `v?((\d+\.?)+)([^\d]?.*)`
func convertToInt(s string) int {
i, err := strconv.Atoi(s)
if err != nil {
return 0
}
return i
}
func (c *GitCommand) GetTags() ([]*Tag, error) {
// get remote branches
remoteBranchesStr, err := c.OSCommand.RunCommandWithOutput(`git tag --list`)
if err != nil {
return nil, err
}
content := utils.TrimTrailingNewline(remoteBranchesStr)
if content == "" {
return nil, nil
}
split := strings.Split(content, "\n")
// first step is to get our remotes from go-git
tags := make([]*Tag, len(split))
for i, tagName := range split {
tags[i] = &Tag{
Name: tagName,
}
}
// now lets sort our tags by name numerically
re := regexp.MustCompile(semverRegex)
// the reason this is complicated is because we're both sorting alphabetically
// and when we're dealing with semver strings
sort.Slice(tags, func(i, j int) bool {
a := tags[i].Name
b := tags[j].Name
matchA := re.FindStringSubmatch(a)
matchB := re.FindStringSubmatch(b)
if len(matchA) > 0 && len(matchB) > 0 {
numbersA := strings.Split(matchA[1], ".")
numbersB := strings.Split(matchB[1], ".")
k := 0
for {
if len(numbersA) == k && len(numbersB) == k {
break
}
if len(numbersA) == k {
return true
}
if len(numbersB) == k {
return false
}
if convertToInt(numbersA[k]) < convertToInt(numbersB[k]) {
return true
}
if convertToInt(numbersA[k]) > convertToInt(numbersB[k]) {
return false
}
k++
}
return strings.ToLower(matchA[3]) < strings.ToLower(matchB[3])
}
return strings.ToLower(a) < strings.ToLower(b)
})
return tags, nil
}
+90 -388
View File
@@ -1,178 +1,68 @@
package commands
import (
"bufio"
"fmt"
"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
beforeExecuteCmd func(*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,
beforeExecuteCmd: func(*exec.Cmd) {},
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
}
func (c *OSCommand) SetBeforeExecuteCmd(cmd func(*exec.Cmd)) {
c.beforeExecuteCmd = cmd
}
type RunCommandOptions struct {
EnvVars []string
}
func (c *OSCommand) RunCommandWithOutputWithOptions(command string, options RunCommandOptions) (string, error) {
c.Log.WithField("command", command).Info("RunCommand")
cmd := c.ExecutableFromString(command)
cmd.Env = append(cmd.Env, options.EnvVars...)
return sanitisedCommandOutput(cmd.CombinedOutput())
}
func (c *OSCommand) RunCommandWithOptions(command string, options RunCommandOptions) error {
_, err := c.RunCommandWithOutputWithOptions(command, options)
return err
return osCommand, nil
}
// RunCommandWithOutput wrapper around commands returning their output and error
// NOTE: If you don't pass any formatArgs we'll just use the command directly,
// however there's a bizarre compiler error/warning when you pass in a formatString
// with a percent sign because it thinks it's supposed to be a formatString when
// in that case it's not. To get around that error you'll need to define the string
// in a variable and pass the variable into RunCommandWithOutput.
func (c *OSCommand) RunCommandWithOutput(formatString string, formatArgs ...interface{}) (string, error) {
command := formatString
if formatArgs != nil {
command = fmt.Sprintf(formatString, formatArgs...)
}
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) {
c.beforeExecuteCmd(cmd)
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
func (c *OSCommand) RunCommand(formatString string, formatArgs ...interface{}) error {
_, err := c.RunCommandWithOutput(formatString, formatArgs...)
func (c *OSCommand) RunCommand(command string) error {
_, err := c.RunCommandWithOutput(command)
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) {
@@ -180,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 {
@@ -231,237 +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
}
// CreateFileWithContent creates a file with the given content
func (c *OSCommand) CreateFileWithContent(path string, content string) error {
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
c.Log.Error(err)
return err
}
if err := ioutil.WriteFile(path, []byte(content), 0644); err != nil {
c.Log.Error(err)
return WrapError(err)
}
return nil
}
// Remove removes a file or directory at the specified path
func (c *OSCommand) Remove(filename string) error {
err := os.RemoveAll(filename)
return WrapError(err)
}
// FileExists checks whether a file exists at the specified path
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 {
c.beforeExecuteCmd(cmd)
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
}
func Kill(cmd *exec.Cmd) error {
if cmd.Process == nil {
// somebody got to it before we were able to, poor bastard
return nil
}
return cmd.Process.Kill()
}
func RunLineOutputCmd(cmd *exec.Cmd, onLine func(line string) (bool, error)) error {
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
return err
}
scanner := bufio.NewScanner(stdoutPipe)
scanner.Split(bufio.ScanLines)
if err := cmd.Start(); err != nil {
return err
}
for scanner.Scan() {
line := scanner.Text()
stop, err := onLine(line)
if err != nil {
return err
}
if stop {
cmd.Process.Kill()
break
}
}
cmd.Wait()
return nil
}
func (c *OSCommand) CopyToClipboard(str string) error {
commandTemplate := c.Config.GetUserConfig().GetString("os.copyToClipboardCommand")
templateValues := map[string]string{
"str": c.Quote(str),
}
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
return c.RunCommand(command)
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: "\\'",
}
}
-229
View File
@@ -1,229 +0,0 @@
package commands
import (
"sort"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
type fileInfo struct {
mode int // one of WHOLE/PART
includedLineIndices []int
diff string
}
type applyPatchFunc func(patch string, flags ...string) error
// PatchManager manages the building of a patch for a commit to be applied to another commit (or the working tree, or removed from the current commit)
type PatchManager struct {
CommitSha string
fileInfoMap map[string]*fileInfo
Log *logrus.Entry
ApplyPatch applyPatchFunc
}
// NewPatchManager returns a new PatchModifier
func NewPatchManager(log *logrus.Entry, applyPatch applyPatchFunc) *PatchManager {
return &PatchManager{
Log: log,
ApplyPatch: applyPatch,
}
}
// NewPatchManager returns a new PatchModifier
func (p *PatchManager) Start(commitSha string, diffMap map[string]string) {
p.CommitSha = commitSha
p.fileInfoMap = map[string]*fileInfo{}
for filename, diff := range diffMap {
p.fileInfoMap[filename] = &fileInfo{
mode: UNSELECTED,
diff: diff,
}
}
}
func (p *PatchManager) AddFile(filename string) {
p.fileInfoMap[filename].mode = WHOLE
p.fileInfoMap[filename].includedLineIndices = nil
}
func (p *PatchManager) RemoveFile(filename string) {
p.fileInfoMap[filename].mode = UNSELECTED
p.fileInfoMap[filename].includedLineIndices = nil
}
func (p *PatchManager) ToggleFileWhole(filename string) {
info := p.fileInfoMap[filename]
switch info.mode {
case UNSELECTED:
p.AddFile(filename)
case WHOLE:
p.RemoveFile(filename)
case PART:
p.AddFile(filename)
}
}
func getIndicesForRange(first, last int) []int {
indices := []int{}
for i := first; i <= last; i++ {
indices = append(indices, i)
}
return indices
}
func (p *PatchManager) AddFileLineRange(filename string, firstLineIdx, lastLineIdx int) {
info := p.fileInfoMap[filename]
info.mode = PART
info.includedLineIndices = utils.UnionInt(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx))
}
func (p *PatchManager) RemoveFileLineRange(filename string, firstLineIdx, lastLineIdx int) {
info := p.fileInfoMap[filename]
info.mode = PART
info.includedLineIndices = utils.DifferenceInt(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx))
if len(info.includedLineIndices) == 0 {
p.RemoveFile(filename)
}
}
func (p *PatchManager) RenderPlainPatchForFile(filename string, reverse bool, keepOriginalHeader bool) string {
info := p.fileInfoMap[filename]
if info == nil {
return ""
}
switch info.mode {
case WHOLE:
// use the whole diff
// the reverse flag is only for part patches so we're ignoring it here
return info.diff
case PART:
// generate a new diff with just the selected lines
m := NewPatchModifier(p.Log, filename, info.diff)
return m.ModifiedPatchForLines(info.includedLineIndices, reverse, keepOriginalHeader)
default:
return ""
}
}
func (p *PatchManager) RenderPatchForFile(filename string, plain bool, reverse bool, keepOriginalHeader bool) string {
patch := p.RenderPlainPatchForFile(filename, reverse, keepOriginalHeader)
if plain {
return patch
}
parser, err := NewPatchParser(p.Log, patch)
if err != nil {
// swallowing for now
return ""
}
// not passing included lines because we don't want to see them in the secondary panel
return parser.Render(-1, -1, nil)
}
func (p *PatchManager) RenderEachFilePatch(plain bool) []string {
// sort files by name then iterate through and render each patch
filenames := make([]string, len(p.fileInfoMap))
index := 0
for filename := range p.fileInfoMap {
filenames[index] = filename
index++
}
sort.Strings(filenames)
output := []string{}
for _, filename := range filenames {
patch := p.RenderPatchForFile(filename, plain, false, true)
if patch != "" {
output = append(output, patch)
}
}
return output
}
func (p *PatchManager) RenderAggregatedPatchColored(plain bool) string {
result := ""
for _, patch := range p.RenderEachFilePatch(plain) {
if patch != "" {
result += patch + "\n"
}
}
return result
}
func (p *PatchManager) GetFileStatus(filename string) int {
info := p.fileInfoMap[filename]
if info == nil {
return UNSELECTED
}
return info.mode
}
func (p *PatchManager) GetFileIncLineIndices(filename string) []int {
info := p.fileInfoMap[filename]
if info == nil {
return []int{}
}
return info.includedLineIndices
}
func (p *PatchManager) ApplyPatches(reverse bool) error {
// for whole patches we'll apply the patch in reverse
// but for part patches we'll apply a reverse patch forwards
for filename, info := range p.fileInfoMap {
if info.mode == UNSELECTED {
continue
}
applyFlags := []string{"index", "3way"}
reverseOnGenerate := false
if reverse {
if info.mode == WHOLE {
applyFlags = append(applyFlags, "reverse")
} else {
reverseOnGenerate = true
}
}
var err error
// first run we try with the original header, then without
for _, keepOriginalHeader := range []bool{true, false} {
patch := p.RenderPatchForFile(filename, true, reverseOnGenerate, keepOriginalHeader)
if patch == "" {
continue
}
if err = p.ApplyPatch(patch, applyFlags...); err != nil {
continue
}
break
}
if err != nil {
return err
}
}
return nil
}
// clears the patch
func (p *PatchManager) Reset() {
p.CommitSha = ""
p.fileInfoMap = map[string]*fileInfo{}
}
func (p *PatchManager) CommitSelected() bool {
return p.CommitSha != ""
}
func (p *PatchManager) IsEmpty() bool {
for _, fileInfo := range p.fileInfoMap {
if fileInfo.mode == WHOLE || (fileInfo.mode == PART && len(fileInfo.includedLineIndices) > 0) {
return false
}
}
return true
}
-260
View File
@@ -1,260 +0,0 @@
package commands
import (
"fmt"
"regexp"
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
var hunkHeaderRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)[^\+]+\+(\d+)[^@]+@@(.*)$`)
var patchHeaderRegexp = regexp.MustCompile(`(?ms)(^diff.*?)^@@`)
type PatchHunk struct {
header string
FirstLineIdx int
LastLineIdx int
bodyLines []string
}
func newHunk(header string, body string, firstLineIdx int) *PatchHunk {
bodyLines := strings.SplitAfter(header+body, "\n")[1:] // dropping the header line
return &PatchHunk{
header: header,
FirstLineIdx: firstLineIdx,
LastLineIdx: firstLineIdx + len(bodyLines),
bodyLines: bodyLines,
}
}
func (hunk *PatchHunk) updatedLines(lineIndices []int, reverse bool) []string {
skippedNewlineMessageIndex := -1
newLines := []string{}
lineIdx := hunk.FirstLineIdx
for _, line := range hunk.bodyLines {
lineIdx++ // incrementing at the start to skip the header line
if line == "" {
break
}
isLineSelected := utils.IncludesInt(lineIndices, lineIdx)
firstChar, content := line[:1], line[1:]
transformedFirstChar := transformedFirstChar(firstChar, reverse, isLineSelected)
if isLineSelected || (transformedFirstChar == "\\" && skippedNewlineMessageIndex != lineIdx) || transformedFirstChar == " " {
newLines = append(newLines, transformedFirstChar+content)
continue
}
if transformedFirstChar == "+" {
// we don't want to include the 'newline at end of file' line if it involves an addition we're not including
skippedNewlineMessageIndex = lineIdx + 1
}
}
return newLines
}
func transformedFirstChar(firstChar string, reverse bool, isLineSelected bool) string {
if reverse {
if !isLineSelected && firstChar == "+" {
return " "
} else if firstChar == "-" {
return "+"
} else if firstChar == "+" {
return "-"
} else {
return firstChar
}
}
if !isLineSelected && firstChar == "-" {
return " "
}
return firstChar
}
func (hunk *PatchHunk) formatHeader(oldStart int, oldLength int, newStart int, newLength int, heading string) string {
return fmt.Sprintf("@@ -%d,%d +%d,%d @@%s\n", oldStart, oldLength, newStart, newLength, heading)
}
func (hunk *PatchHunk) formatWithChanges(lineIndices []int, reverse bool, startOffset int) (int, string) {
bodyLines := hunk.updatedLines(lineIndices, reverse)
startOffset, header, ok := hunk.updatedHeader(bodyLines, startOffset, reverse)
if !ok {
return startOffset, ""
}
return startOffset, header + strings.Join(bodyLines, "")
}
func (hunk *PatchHunk) updatedHeader(newBodyLines []string, startOffset int, reverse bool) (int, string, bool) {
changeCount := 0
oldLength := 0
newLength := 0
for _, line := range newBodyLines {
switch line[:1] {
case "+":
newLength++
changeCount++
case "-":
oldLength++
changeCount++
case " ":
oldLength++
newLength++
}
}
if changeCount == 0 {
// if nothing has changed we just return nothing
return startOffset, "", false
}
// get oldstart, newstart, and heading from header
match := hunkHeaderRegexp.FindStringSubmatch(hunk.header)
var oldStart int
if reverse {
oldStart = mustConvertToInt(match[2])
} else {
oldStart = mustConvertToInt(match[1])
}
heading := match[3]
var newStartOffset int
// if the hunk went from zero to positive length, we need to increment the starting point by one
// if the hunk went from positive to zero length, we need to decrement the starting point by one
if oldLength == 0 {
newStartOffset = 1
} else if newLength == 0 {
newStartOffset = -1
} else {
newStartOffset = 0
}
newStart := oldStart + startOffset + newStartOffset
newStartOffset = startOffset + newLength - oldLength
formattedHeader := hunk.formatHeader(oldStart, oldLength, newStart, newLength, heading)
return newStartOffset, formattedHeader, true
}
func mustConvertToInt(s string) int {
i, err := strconv.Atoi(s)
if err != nil {
panic(err)
}
return i
}
func GetHeaderFromDiff(diff string) string {
match := patchHeaderRegexp.FindStringSubmatch(diff)
if len(match) <= 1 {
return ""
}
return match[1]
}
func GetHunksFromDiff(diff string) []*PatchHunk {
headers := hunkHeaderRegexp.FindAllString(diff, -1)
bodies := hunkHeaderRegexp.Split(diff, -1)[1:] // discarding top bit
headerFirstLineIndices := []int{}
for lineIdx, line := range strings.Split(diff, "\n") {
if strings.HasPrefix(line, "@@ -") {
headerFirstLineIndices = append(headerFirstLineIndices, lineIdx)
}
}
hunks := make([]*PatchHunk, len(headers))
for index, header := range headers {
hunks[index] = newHunk(header, bodies[index], headerFirstLineIndices[index])
}
return hunks
}
type PatchModifier struct {
Log *logrus.Entry
filename string
hunks []*PatchHunk
header string
}
func NewPatchModifier(log *logrus.Entry, filename string, diffText string) *PatchModifier {
return &PatchModifier{
Log: log,
filename: filename,
hunks: GetHunksFromDiff(diffText),
header: GetHeaderFromDiff(diffText),
}
}
func (d *PatchModifier) ModifiedPatchForLines(lineIndices []int, reverse bool, keepOriginalHeader bool) string {
// step one is getting only those hunks which we care about
hunksInRange := []*PatchHunk{}
outer:
for _, hunk := range d.hunks {
// if there is any line in our lineIndices array that the hunk contains, we append it
for _, lineIdx := range lineIndices {
if lineIdx >= hunk.FirstLineIdx && lineIdx <= hunk.LastLineIdx {
hunksInRange = append(hunksInRange, hunk)
continue outer
}
}
}
// step 2 is collecting all the hunks with new headers
startOffset := 0
formattedHunks := ""
var formattedHunk string
for _, hunk := range hunksInRange {
startOffset, formattedHunk = hunk.formatWithChanges(lineIndices, reverse, startOffset)
formattedHunks += formattedHunk
}
if formattedHunks == "" {
return ""
}
var fileHeader string
// for staging/unstaging lines we don't want the original header because
// it makes git confused e.g. when dealing with deleted/added files
// but with building and applying patches the original header gives git
// information it needs to cleanly apply patches
if keepOriginalHeader {
fileHeader = d.header
} else {
fileHeader = fmt.Sprintf("--- a/%s\n+++ b/%s\n", d.filename, d.filename)
}
return fileHeader + formattedHunks
}
func (d *PatchModifier) ModifiedPatchForRange(firstLineIdx int, lastLineIdx int, reverse bool, keepOriginalHeader bool) string {
// generate array of consecutive line indices from our range
selectedLines := []int{}
for i := firstLineIdx; i <= lastLineIdx; i++ {
selectedLines = append(selectedLines, i)
}
return d.ModifiedPatchForLines(selectedLines, reverse, keepOriginalHeader)
}
func (d *PatchModifier) OriginalPatchLength() int {
if len(d.hunks) == 0 {
return 0
}
return d.hunks[len(d.hunks)-1].LastLineIdx
}
func ModifiedPatchForRange(log *logrus.Entry, filename string, diffText string, firstLineIdx int, lastLineIdx int, reverse bool, keepOriginalHeader bool) string {
p := NewPatchModifier(log, filename, diffText)
return p.ModifiedPatchForRange(firstLineIdx, lastLineIdx, reverse, keepOriginalHeader)
}
-511
View File
@@ -1,511 +0,0 @@
package commands
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
const simpleDiff = `diff --git a/filename b/filename
index dcd3485..1ba5540 100644
--- a/filename
+++ b/filename
@@ -1,5 +1,5 @@
apple
-orange
+grape
...
...
...
`
const addNewlineToEndOfFile = `diff --git a/filename b/filename
index 80a73f1..e48a11c 100644
--- a/filename
+++ b/filename
@@ -60,4 +60,4 @@ grape
...
...
...
-last line
\ No newline at end of file
+last line
`
const removeNewlinefromEndOfFile = `diff --git a/filename b/filename
index e48a11c..80a73f1 100644
--- a/filename
+++ b/filename
@@ -60,4 +60,4 @@ grape
...
...
...
-last line
+last line
\ No newline at end of file
`
const twoHunks = `diff --git a/filename b/filename
index e48a11c..b2ab81b 100644
--- a/filename
+++ b/filename
@@ -1,5 +1,5 @@
apple
-grape
+orange
...
...
...
@@ -8,6 +8,8 @@ grape
...
...
...
+pear
+lemon
...
...
...
`
const newFile = `diff --git a/newfile b/newfile
new file mode 100644
index 0000000..4e680cc
--- /dev/null
+++ b/newfile
@@ -0,0 +1,3 @@
+apple
+orange
+grape
`
const addNewlineToPreviouslyEmptyFile = `diff --git a/newfile b/newfile
index e69de29..c6568ea 100644
--- a/newfile
+++ b/newfile
@@ -0,0 +1 @@
+new line
\ No newline at end of file
`
// TestModifyPatchForRange is a function.
func TestModifyPatchForRange(t *testing.T) {
type scenario struct {
testName string
filename string
diffText string
firstLineIndex int
lastLineIndex int
reverse bool
expected string
}
scenarios := []scenario{
{
testName: "nothing selected",
filename: "filename",
firstLineIndex: -1,
lastLineIndex: -1,
reverse: false,
diffText: simpleDiff,
expected: "",
},
{
testName: "only context selected",
filename: "filename",
firstLineIndex: 5,
lastLineIndex: 5,
reverse: false,
diffText: simpleDiff,
expected: "",
},
{
testName: "whole range selected",
filename: "filename",
firstLineIndex: 0,
lastLineIndex: 11,
reverse: false,
diffText: simpleDiff,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,5 @@
apple
-orange
+grape
...
...
...
`,
},
{
testName: "only removal selected",
filename: "filename",
firstLineIndex: 6,
lastLineIndex: 6,
reverse: false,
diffText: simpleDiff,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,4 @@
apple
-orange
...
...
...
`,
},
{
testName: "only addition selected",
filename: "filename",
firstLineIndex: 7,
lastLineIndex: 7,
reverse: false,
diffText: simpleDiff,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,6 @@
apple
orange
+grape
...
...
...
`,
},
{
testName: "range that extends beyond diff bounds",
filename: "filename",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: false,
diffText: simpleDiff,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,5 @@
apple
-orange
+grape
...
...
...
`,
},
{
testName: "whole range reversed",
filename: "filename",
firstLineIndex: 0,
lastLineIndex: 11,
reverse: true,
diffText: simpleDiff,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,5 @@
apple
+orange
-grape
...
...
...
`,
},
{
testName: "removal reversed",
filename: "filename",
firstLineIndex: 6,
lastLineIndex: 6,
reverse: true,
diffText: simpleDiff,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,6 @@
apple
+orange
grape
...
...
...
`,
},
{
testName: "removal reversed",
filename: "filename",
firstLineIndex: 7,
lastLineIndex: 7,
reverse: true,
diffText: simpleDiff,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,4 @@
apple
-grape
...
...
...
`,
},
{
testName: "add newline to end of file",
filename: "filename",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: false,
diffText: addNewlineToEndOfFile,
expected: `--- a/filename
+++ b/filename
@@ -60,4 +60,4 @@ grape
...
...
...
-last line
\ No newline at end of file
+last line
`,
},
{
testName: "add newline to end of file, addition only",
filename: "filename",
firstLineIndex: 8,
lastLineIndex: 8,
reverse: true,
diffText: addNewlineToEndOfFile,
expected: `--- a/filename
+++ b/filename
@@ -60,4 +60,5 @@ grape
...
...
...
+last line
\ No newline at end of file
last line
`,
},
{
testName: "add newline to end of file, removal only",
filename: "filename",
firstLineIndex: 10,
lastLineIndex: 10,
reverse: true,
diffText: addNewlineToEndOfFile,
expected: `--- a/filename
+++ b/filename
@@ -60,4 +60,3 @@ grape
...
...
...
-last line
`,
},
{
testName: "remove newline from end of file",
filename: "filename",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: false,
diffText: removeNewlinefromEndOfFile,
expected: `--- a/filename
+++ b/filename
@@ -60,4 +60,4 @@ grape
...
...
...
-last line
+last line
\ No newline at end of file
`,
},
{
testName: "remove newline from end of file, removal only",
filename: "filename",
firstLineIndex: 8,
lastLineIndex: 8,
reverse: false,
diffText: removeNewlinefromEndOfFile,
expected: `--- a/filename
+++ b/filename
@@ -60,4 +60,3 @@ grape
...
...
...
-last line
`,
},
{
testName: "remove newline from end of file, addition only",
filename: "filename",
firstLineIndex: 9,
lastLineIndex: 9,
reverse: false,
diffText: removeNewlinefromEndOfFile,
expected: `--- a/filename
+++ b/filename
@@ -60,4 +60,5 @@ grape
...
...
...
last line
+last line
\ No newline at end of file
`,
},
{
testName: "staging two whole hunks",
filename: "filename",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: false,
diffText: twoHunks,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,5 @@
apple
-grape
+orange
...
...
...
@@ -8,6 +8,8 @@ grape
...
...
...
+pear
+lemon
...
...
...
`,
},
{
testName: "staging part of both hunks",
filename: "filename",
firstLineIndex: 7,
lastLineIndex: 15,
reverse: false,
diffText: twoHunks,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,6 @@
apple
grape
+orange
...
...
...
@@ -8,6 +9,7 @@ grape
...
...
...
+pear
...
...
...
`,
},
{
testName: "staging part of both hunks, reversed",
filename: "filename",
firstLineIndex: 7,
lastLineIndex: 15,
reverse: true,
diffText: twoHunks,
expected: `--- a/filename
+++ b/filename
@@ -1,5 +1,4 @@
apple
-orange
...
...
...
@@ -8,8 +7,7 @@ grape
...
...
...
-pear
lemon
...
...
...
`,
},
{
testName: "adding a new file",
filename: "newfile",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: false,
diffText: newFile,
expected: `--- a/newfile
+++ b/newfile
@@ -0,0 +1,3 @@
+apple
+orange
+grape
`,
},
{
testName: "adding part of a new file",
filename: "newfile",
firstLineIndex: 6,
lastLineIndex: 7,
reverse: false,
diffText: newFile,
expected: `--- a/newfile
+++ b/newfile
@@ -0,0 +1,2 @@
+apple
+orange
`,
},
{
testName: "adding a new file, reversed",
filename: "newfile",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: true,
diffText: newFile,
expected: `--- a/newfile
+++ b/newfile
@@ -1,3 +0,0 @@
-apple
-orange
-grape
`,
},
{
testName: "adding a new line to a previously empty file",
filename: "newfile",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: false,
diffText: addNewlineToPreviouslyEmptyFile,
expected: `--- a/newfile
+++ b/newfile
@@ -0,0 +1,1 @@
+new line
\ No newline at end of file
`,
},
{
testName: "adding a new line to a previously empty file, reversed",
filename: "newfile",
firstLineIndex: -100,
lastLineIndex: 100,
reverse: true,
diffText: addNewlineToPreviouslyEmptyFile,
expected: `--- a/newfile
+++ b/newfile
@@ -1,1 +0,0 @@
-new line
\ No newline at end of file
`,
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
result := ModifiedPatchForRange(nil, s.filename, s.diffText, s.firstLineIndex, s.lastLineIndex, s.reverse, false)
if !assert.Equal(t, s.expected, result) {
fmt.Println(result)
}
})
}
}
-213
View File
@@ -1,213 +0,0 @@
package commands
import (
"regexp"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
const (
PATCH_HEADER = iota
COMMIT_SHA
COMMIT_DESCRIPTION
HUNK_HEADER
ADDITION
DELETION
CONTEXT
NEWLINE_MESSAGE
)
// the job of this file is to parse a diff, find out where the hunks begin and end, which lines are stageable, and how to find the next hunk from the current position or the next stageable line from the current position.
type PatchLine struct {
Kind int
Content string // something like '+ hello' (note the first character is not removed)
}
type PatchParser struct {
Log *logrus.Entry
PatchLines []*PatchLine
PatchHunks []*PatchHunk
HunkStarts []int
StageableLines []int // rename to mention we're talking about indexes
}
// NewPatchParser builds a new branch list builder
func NewPatchParser(log *logrus.Entry, patch string) (*PatchParser, error) {
hunkStarts, stageableLines, patchLines, err := parsePatch(patch)
if err != nil {
return nil, err
}
patchHunks := GetHunksFromDiff(patch)
return &PatchParser{
Log: log,
HunkStarts: hunkStarts, // deprecated
StageableLines: stageableLines,
PatchLines: patchLines,
PatchHunks: patchHunks,
}, nil
}
// GetHunkContainingLine takes a line index and an offset and finds the hunk
// which contains the line index, then returns the hunk considering the offset.
// e.g. if the offset is 1 it will return the next hunk.
func (p *PatchParser) GetHunkContainingLine(lineIndex int, offset int) *PatchHunk {
if len(p.PatchHunks) == 0 {
return nil
}
for index, hunk := range p.PatchHunks {
if lineIndex >= hunk.FirstLineIdx && lineIndex <= hunk.LastLineIdx {
resultIndex := index + offset
if resultIndex < 0 {
resultIndex = 0
} else if resultIndex > len(p.PatchHunks)-1 {
resultIndex = len(p.PatchHunks) - 1
}
return p.PatchHunks[resultIndex]
}
}
// if your cursor is past the last hunk, select the last hunk
if lineIndex > p.PatchHunks[len(p.PatchHunks)-1].LastLineIdx {
return p.PatchHunks[len(p.PatchHunks)-1]
}
// otherwise select the first
return p.PatchHunks[0]
}
// selected means you've got it highlighted with your cursor
// included means the line has been included in the patch (only applicable when
// building a patch)
func (l *PatchLine) render(selected bool, included bool) string {
content := l.Content
if len(content) == 0 {
content = " " // using the space so that we can still highlight if necessary
}
// for hunk headers we need to start off cyan and then use white for the message
if l.Kind == HUNK_HEADER {
re := regexp.MustCompile("(@@.*?@@)(.*)")
match := re.FindStringSubmatch(content)
return coloredString(color.FgCyan, match[1], selected, included) + coloredString(theme.DefaultTextColor, match[2], selected, false)
}
var colorAttr color.Attribute
switch l.Kind {
case PATCH_HEADER:
colorAttr = color.Bold
case ADDITION:
colorAttr = color.FgGreen
case DELETION:
colorAttr = color.FgRed
case COMMIT_SHA:
colorAttr = color.FgYellow
default:
colorAttr = theme.DefaultTextColor
}
return coloredString(colorAttr, content, selected, included)
}
func coloredString(colorAttr color.Attribute, str string, selected bool, included bool) string {
var cl *color.Color
attributes := []color.Attribute{colorAttr}
if selected {
attributes = append(attributes, theme.SelectedLineBgColor)
}
cl = color.New(attributes...)
var clIncluded *color.Color
if included {
clIncluded = color.New(append(attributes, color.BgGreen)...)
} else {
clIncluded = color.New(attributes...)
}
if len(str) < 2 {
return utils.ColoredStringDirect(str, clIncluded)
}
return utils.ColoredStringDirect(str[:1], clIncluded) + utils.ColoredStringDirect(str[1:], cl)
}
func parsePatch(patch string) ([]int, []int, []*PatchLine, error) {
lines := strings.Split(patch, "\n")
hunkStarts := []int{}
stageableLines := []int{}
pastFirstHunkHeader := false
pastCommitDescription := true
patchLines := make([]*PatchLine, len(lines))
var lineKind int
var firstChar string
for index, line := range lines {
firstChar = " "
if len(line) > 0 {
firstChar = line[:1]
}
if index == 0 && strings.HasPrefix(line, "commit") {
lineKind = COMMIT_SHA
pastCommitDescription = false
} else if !pastCommitDescription {
if strings.HasPrefix(line, "diff") || strings.HasPrefix(line, "---") {
pastCommitDescription = true
lineKind = PATCH_HEADER
} else {
lineKind = COMMIT_DESCRIPTION
}
} else if firstChar == "@" {
pastFirstHunkHeader = true
hunkStarts = append(hunkStarts, index)
lineKind = HUNK_HEADER
} else if pastFirstHunkHeader {
switch firstChar {
case "-":
lineKind = DELETION
stageableLines = append(stageableLines, index)
case "+":
lineKind = ADDITION
stageableLines = append(stageableLines, index)
case "\\":
lineKind = NEWLINE_MESSAGE
case " ":
lineKind = CONTEXT
}
} else {
lineKind = PATCH_HEADER
}
patchLines[index] = &PatchLine{Kind: lineKind, Content: line}
}
return hunkStarts, stageableLines, patchLines, nil
}
// Render returns the coloured string of the diff with any selected lines highlighted
func (p *PatchParser) Render(firstLineIndex int, lastLineIndex int, incLineIndices []int) string {
renderedLines := make([]string, len(p.PatchLines))
for index, patchLine := range p.PatchLines {
selected := index >= firstLineIndex && index <= lastLineIndex
included := utils.IncludesInt(incLineIndices, index)
renderedLines[index] = patchLine.render(selected, included)
}
result := strings.Join(renderedLines, "\n")
if strings.TrimSpace(utils.Decolorise(result)) == "" {
return ""
}
return result
}
// GetNextStageableLineIndex takes a line index and returns the line index of the next stageable line
// note this will actually include the current index if it is stageable
func (p *PatchParser) GetNextStageableLineIndex(currentIndex int) int {
for _, lineIndex := range p.StageableLines {
if lineIndex >= currentIndex {
return lineIndex
}
}
return p.StageableLines[len(p.StageableLines)-1]
}
-185
View File
@@ -1,185 +0,0 @@
package commands
import "github.com/go-errors/errors"
// DeletePatchesFromCommit applies a patch in reverse for a commit
func (c *GitCommand) DeletePatchesFromCommit(commits []*Commit, commitIndex int, p *PatchManager) error {
if err := c.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil {
return err
}
// apply each patch in reverse
if err := p.ApplyPatches(true); err != nil {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
}
return err
}
// time to amend the selected commit
if _, err := c.AmendHead(); err != nil {
return err
}
c.onSuccessfulContinue = func() error {
c.PatchManager.Reset()
return nil
}
// continue
return c.GenericMerge("rebase", "continue")
}
func (c *GitCommand) MovePatchToSelectedCommit(commits []*Commit, sourceCommitIdx int, destinationCommitIdx int, p *PatchManager) error {
if sourceCommitIdx < destinationCommitIdx {
if err := c.BeginInteractiveRebaseForCommit(commits, destinationCommitIdx); err != nil {
return err
}
// apply each patch forward
if err := p.ApplyPatches(false); err != nil {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
}
return err
}
// amend the destination commit
if _, err := c.AmendHead(); err != nil {
return err
}
c.onSuccessfulContinue = func() error {
c.PatchManager.Reset()
return nil
}
// continue
return c.GenericMerge("rebase", "continue")
}
if len(commits)-1 < sourceCommitIdx {
return errors.New("index outside of range of commits")
}
// we can make this GPG thing possible it just means we need to do this in two parts:
// one where we handle the possibility of a credential request, and the other
// where we continue the rebase
if c.usingGpg() {
return errors.New(c.Tr.SLocalize("DisabledForGPG"))
}
baseIndex := sourceCommitIdx + 1
todo := ""
for i, commit := range commits[0:baseIndex] {
a := "pick"
if i == sourceCommitIdx || i == destinationCommitIdx {
a = "edit"
}
todo = a + " " + commit.Sha + " " + commit.Name + "\n" + todo
}
cmd, err := c.PrepareInteractiveRebaseCommand(commits[baseIndex].Sha, todo, true)
if err != nil {
return err
}
if err := c.OSCommand.RunPreparedCommand(cmd); err != nil {
return err
}
// apply each patch in reverse
if err := p.ApplyPatches(true); err != nil {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
}
return err
}
// amend the source commit
if _, err := c.AmendHead(); err != nil {
return err
}
if c.onSuccessfulContinue != nil {
return errors.New("You are midway through another rebase operation. Please abort to start again")
}
c.onSuccessfulContinue = func() error {
// now we should be up to the destination, so let's apply forward these patches to that.
// ideally we would ensure we're on the right commit but I'm not sure if that check is necessary
if err := p.ApplyPatches(false); err != nil {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
}
return err
}
// amend the destination commit
if _, err := c.AmendHead(); err != nil {
return err
}
c.onSuccessfulContinue = func() error {
c.PatchManager.Reset()
return nil
}
return c.GenericMerge("rebase", "continue")
}
return c.GenericMerge("rebase", "continue")
}
func (c *GitCommand) PullPatchIntoIndex(commits []*Commit, commitIdx int, p *PatchManager, stash bool) error {
if stash {
if err := c.StashSave(c.Tr.SLocalize("StashPrefix") + commits[commitIdx].Sha); err != nil {
return err
}
}
if err := c.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil {
return err
}
if err := p.ApplyPatches(true); err != nil {
if c.WorkingTreeState() == "rebasing" {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
}
}
return err
}
// amend the commit
if _, err := c.AmendHead(); err != nil {
return err
}
if c.onSuccessfulContinue != nil {
return errors.New("You are midway through another rebase operation. Please abort to start again")
}
c.onSuccessfulContinue = func() error {
// add patches to index
if err := p.ApplyPatches(false); err != nil {
if c.WorkingTreeState() == "rebasing" {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
}
}
return err
}
if stash {
if err := c.StashDo(0, "apply"); err != nil {
return err
}
}
c.PatchManager.Reset()
return nil
}
return c.GenericMerge("rebase", "continue")
}
-143
View File
@@ -1,143 +0,0 @@
package commands
import (
"fmt"
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/config"
)
// 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
}
// NewService builds a Service based on the host type
func NewService(typeName string, repositoryDomain string, siteDomain string) *Service {
var service *Service
switch typeName {
case "github":
service = &Service{
Name: repositoryDomain,
PullRequestURL: fmt.Sprintf("https://%s%s", siteDomain, "/%s/%s/compare/%s?expand=1"),
}
case "bitbucket":
service = &Service{
Name: repositoryDomain,
PullRequestURL: fmt.Sprintf("https://%s%s", siteDomain, "/%s/%s/pull-requests/new?source=%s&t=1"),
}
case "gitlab":
service = &Service{
Name: repositoryDomain,
PullRequestURL: fmt.Sprintf("https://%s%s", siteDomain, "/%s/%s/merge_requests/new?merge_request[source_branch]=%s"),
}
}
return service
}
func getServices(config config.AppConfigurer) []*Service {
services := []*Service{
NewService("github", "github.com", "github.com"),
NewService("bitbucket", "bitbucket.org", "bitbucket.org"),
NewService("gitlab", "gitlab.com", "gitlab.com"),
}
configServices := config.GetUserConfig().GetStringMapString("services")
for repoDomain, typeAndDomain := range configServices {
splitData := strings.Split(typeAndDomain, ":")
if len(splitData) != 2 {
// TODO log this misconfiguration
continue
}
service := NewService(splitData[0], repoDomain, splitData[1])
if service == nil {
// TODO log this unsupported service
continue
}
services = append(services, service)
}
return services
}
// NewPullRequest creates new instance of PullRequest
func NewPullRequest(gitCommand *GitCommand) *PullRequest {
return &PullRequest{
GitServices: getServices(gitCommand.Config),
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 := strings.Join(splits[3:len(splits)-1], "/")
repo := strings.TrimSuffix(splits[len(splits)-1], ".git")
return &RepoInformation{
Owner: owner,
Repository: repo,
}
}
tmpSplit := strings.Split(url, ":")
splits := strings.Split(tmpSplit[1], "/")
owner := strings.Join(splits[0:len(splits)-1], "/")
repo := strings.TrimSuffix(splits[len(splits)-1], ".git")
return &RepoInformation{
Owner: owner,
Repository: repo,
}
}
-161
View File
@@ -1,161 +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}}")
gitCommand.Config.GetUserConfig().Set("services", map[string]string{
// valid configuration for a custom service URL
"git.work.com": "gitlab:code.work.com",
// invalid configurations for a custom service URL
"invalid.work.com": "noservice:invalid.work.com",
"noservice.work.com": "noservice.work.com",
})
dummyPullRequest := NewPullRequest(gitCommand)
s.test(dummyPullRequest.Create(s.branch))
})
}
}
-8
View File
@@ -1,8 +0,0 @@
package commands
// Remote : A git remote
type Remote struct {
Name string
Urls []string
Branches []*RemoteBranch
}
-11
View File
@@ -1,11 +0,0 @@
package commands
// Remote Branch : A git remote branch
type RemoteBranch struct {
Name string
RemoteName string
}
func (r *RemoteBranch) FullName() string {
return r.RemoteName + "/" + r.Name
}
-13
View File
@@ -1,13 +0,0 @@
package commands
import "fmt"
// StashEntry : A git stash entry
type StashEntry struct {
Index int
Name string
}
func (s *StashEntry) RefName() string {
return fmt.Sprintf("stash@{%d}", s.Index)
}
-6
View File
@@ -1,6 +0,0 @@
package commands
// Tag : A git tag
type Tag struct {
Name string
}
View File
View File
+72 -303
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, interface{}) 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,291 +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 string, value interface{}) error {
// reloading the user config directly (without defaults) so that we're not
// writing any defaults back to the user's config
v, _, err := LoadConfig("config", false)
// 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: true
skipUnstageLineWarning: false
skipStashWarning: true
sidePanelWidth: 0.3333
theme:
lightTheme: false
activeBorderColor:
- green
- bold
inactiveBorderColor:
- white
optionsTextColor:
- blue
selectedLineBgColor:
- blue
commitLength:
show: true
git:
paging:
colorArg: always
useConfig: false
merging:
manualCommit: false
args: ""
skipHookPrefix: 'WIP'
autoFetch: true
update:
method: prompt # can be: prompt | background | never
days: 14 # how often a update is checked for
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
splashUpdatesIndex: 0
confirmOnQuit: false
keybinding:
universal:
quit: 'q'
quit-alt1: '<c-c>'
return: '<esc>'
quitWithoutChangingDirectory: 'Q'
togglePanel: '<tab>'
prevItem: '<up>'
nextItem: '<down>'
prevItem-alt: 'k'
nextItem-alt: 'j'
prevPage: ','
nextPage: '.'
gotoTop: '<'
gotoBottom: '>'
prevBlock: '<left>'
nextBlock: '<right>'
prevBlock-alt: 'h'
nextBlock-alt: 'l'
nextMatch: 'n'
prevMatch: 'N'
startSearch: '/'
optionMenu: 'x'
optionMenu-alt1: '?'
select: '<space>'
goInto: '<enter>'
remove: 'd'
new: 'n'
edit: 'e'
openFile: 'o'
scrollUpMain: '<pgup>'
scrollDownMain: '<pgdown>'
scrollUpMain-alt1: 'K'
scrollDownMain-alt1: 'J'
scrollUpMain-alt2: '<c-u>'
scrollDownMain-alt2: '<c-d>'
executeCustomCommand: ':'
createRebaseOptionsMenu: 'm'
pushFiles: 'P'
pullFiles: 'p'
refresh: 'R'
createPatchOptionsMenu: '<c-p>'
nextTab: ']'
prevTab: '['
nextScreenMode: '+'
prevScreenMode: '_'
undo: 'z'
redo: '<c-z>'
filteringMenu: <c-s>
diffingMenu: '<c-e>'
copyToClipboard: '<c-o>'
status:
checkForUpdate: 'u'
recentRepos: '<enter>'
files:
commitChanges: 'c'
commitChangesWithoutHook: 'w'
amendLastCommit: 'A'
commitChangesWithEditor: 'C'
ignoreFile: 'i'
refreshFiles: 'r'
stashAllChanges: 's'
viewStashOptions: 'S'
toggleStagedAll: 'a'
viewResetOptions: 'D'
fetch: 'f'
branches:
createPullRequest: 'o'
checkoutBranchByName: 'c'
forceCheckoutBranch: 'F'
rebaseBranch: 'r'
renameBranch: 'R'
mergeIntoCurrentBranch: 'M'
viewGitFlowOptions: 'i'
fastForward: 'f'
pushTag: 'P'
setUpstream: 'u'
fetchRemote: 'f'
commits:
squashDown: 's'
renameCommit: 'r'
renameCommitWithEditor: 'R'
viewResetOptions: 'g'
markCommitAsFixup: 'f'
createFixupCommit: 'F'
squashAboveCommits: 'S'
moveDownCommit: '<c-j>'
moveUpCommit: '<c-k>'
amendToCommit: 'A'
pickCommit: 'p'
revertCommit: 't'
cherryPickCopy: 'c'
cherryPickCopyRange: 'C'
pasteCommits: 'v'
tagCommit: 'T'
checkoutCommit: '<space>'
resetCherryPick: '<c-R>'
stash:
popStash: 'g'
commitFiles:
checkoutCommitFile: 'c'
main:
toggleDragSelect: 'v'
toggleDragSelect-alt: 'V'
toggleSelectHunk: 'a'
pickBothHunks: 'b'
`)
}
// 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
-12
View File
@@ -1,12 +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}}'
copyToClipboardCommand: 'bash -c "echo -n {{str}} | pbcopy"'`)
}
-10
View File
@@ -1,10 +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"'
copyToClipboardCommand: 'bash -c "echo -n {{str}} | xclip -selection clipboard"'`)
}
-10
View File
@@ -1,10 +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}}"'
copyToClipboardCommand: 'cmd \c "echo -n {{str}} > /dev/clipboard"`)
}
+163
View File
@@ -0,0 +1,163 @@
package git
import (
"regexp"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/Sirupsen/logrus"
"gopkg.in/src-d/go-git.v4/plumbing"
)
// 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
// BranchListBuilder returns a list of Branch objects for the current repo
type BranchListBuilder struct {
Log *logrus.Logger
GitCommand *commands.GitCommand
}
// NewBranchListBuilder builds a new branch list builder
func NewBranchListBuilder(log *logrus.Logger, gitCommand *commands.GitCommand) (*BranchListBuilder, error) {
return &BranchListBuilder{
Log: log,
GitCommand: gitCommand,
}, nil
}
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 {
branchName, err = b.GitCommand.OSCommand.RunCommandWithOutput("git rev-parse --short HEAD")
if err != nil {
panic(err.Error())
}
}
return commands.Branch{Name: strings.TrimSpace(branchName), Recency: " *"}
}
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
}
branchLines := utils.SplitLines(rawString)
for _, line := range branchLines {
timeNumber, timeUnit, branchName := branchInfoFromLine(line)
timeUnit = abbreviatedTimeUnit(timeUnit)
branch := commands.Branch{Name: branchName, Recency: timeNumber + timeUnit}
branches = append(branches, branch)
}
return branches
}
func (b *BranchListBuilder) obtainSafeBranches() []commands.Branch {
branches := make([]commands.Branch, 0)
bIter, err := b.GitCommand.Repo.Branches()
if err != nil {
panic(err)
}
err = bIter.ForEach(func(b *plumbing.Reference) error {
name := b.Name().Short()
branches = append(branches, commands.Branch{Name: name})
return nil
})
return branches
}
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)
}
}
return finalBranches
}
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
}
}
return reflogBranch.Name
}
// Build the list of branches for the current repo
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)
}
branches = b.appendNewBranches(branches, reflogBranches, safeBranches, true)
branches = b.appendNewBranches(branches, safeBranches, branches, false)
return branches
}
func branchIncluded(branchName string, branches []commands.Branch) bool {
for _, existingBranch := range branches {
if strings.ToLower(existingBranch.Name) == strings.ToLower(branchName) {
return true
}
}
return false
}
func uniqueByName(branches []commands.Branch) []commands.Branch {
finalBranches := make([]commands.Branch, 0)
for _, branch := range branches {
if branchIncluded(branch.Name, finalBranches) {
continue
}
finalBranches = append(finalBranches, branch)
}
return finalBranches
}
// A line will have the form '10 days ago master' so we need to strip out the
// useful information from that into timeNumber, timeUnit, and branchName
func branchInfoFromLine(line string) (string, string, string) {
r := regexp.MustCompile("\\|.*\\s")
line = r.ReplaceAllString(line, " ")
words := strings.Split(line, " ")
return words[0], words[1], words[len(words)-1]
}
func abbreviatedTimeUnit(timeUnit string) string {
r := regexp.MustCompile("s$")
timeUnit = r.ReplaceAllString(timeUnit, "")
timeUnitMap := map[string]string{
"hour": "h",
"minute": "m",
"second": "s",
"week": "w",
"year": "y",
"day": "d",
"month": "m",
}
return timeUnitMap[timeUnit]
}
-81
View File
@@ -1,81 +0,0 @@
package gui
import (
"time"
"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.statusManager.addWaitingStatus(name)
defer func() {
gui.statusManager.removeStatus(name)
}()
go func() {
ticker := time.NewTicker(time.Millisecond * 50)
defer ticker.Stop()
for range ticker.C {
appStatus := gui.statusManager.getStatusString()
gui.Log.Warn(appStatus)
if appStatus == "" {
return
}
gui.renderString(gui.g, "appStatus", appStatus)
}
}()
if err := f(); err != nil {
gui.g.Update(func(g *gocui.Gui) error {
return gui.surfaceError(err)
})
}
}()
return nil
}
+95 -470
View File
@@ -6,526 +6,151 @@ import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/utils"
"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
}
gui.State.SplitMainPanel = false
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
gui.getMainView().Title = "Log"
// This really shouldn't happen: there should always be a master branch
if len(gui.State.Branches) == 0 {
return gui.newStringTask("main", gui.Tr.SLocalize("NoBranchesThisRepo"))
}
branch := gui.getSelectedBranch()
v.FocusPoint(0, gui.State.Panels.Branches.SelectedLine)
if gui.inDiffMode() {
return gui.renderDiff()
}
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.GetBranchGraphCmdStr(branch.Name),
)
if err := gui.newCmdTask("main", cmd); err != nil {
gui.Log.Error(err)
}
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() {
reflogCommits := gui.State.FilteredReflogCommits
if gui.inFilterMode() {
// in filter mode we filter our reflog commits to just those containing the path
// however we need all the reflog entries to populate the recencies of our branches
// which allows us to order them correctly. So if we're filtering we'll just
// manually load all the reflog commits here
var err error
reflogCommits, _, err = gui.GitCommand.GetReflogCommits(nil, "")
if err != nil {
gui.Log.Error(err)
}
}
builder, err := commands.NewBranchListBuilder(gui.Log, gui.GitCommand, reflogCommits)
if err != nil {
_ = gui.surfaceError(err)
}
gui.State.Branches = builder.Build()
// TODO: if we're in the remotes view and we've just deleted a remote we need to refresh accordingly
if gui.getBranchesView().Context == "local-branches" {
_ = gui.renderLocalBranchesWithSelection()
}
gui.refreshStatus()
}
func (gui *Gui) renderLocalBranchesWithSelection() error {
branchesView := gui.getBranchesView()
gui.refreshSelectedLine(&gui.State.Panels.Branches.SelectedLine, len(gui.State.Branches))
displayStrings := presentation.GetBranchListDisplayStrings(gui.State.Branches, gui.State.ScreenMode != SCREEN_NORMAL, gui.State.Diff.Ref)
gui.renderDisplayStrings(branchesView, displayStrings)
if gui.g.CurrentView() == branchesView {
if err := gui.handleBranchSelect(gui.g, branchesView); err != nil {
return gui.surfaceError(err)
}
}
return nil
}
// specific functions
func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error {
if gui.State.Panels.Branches.SelectedLine == -1 {
return nil
index := gui.getItemPosition(v)
if index == 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("AlreadyCheckedOutBranch"))
}
if gui.State.Panels.Branches.SelectedLine == 0 {
return gui.createErrorPanel(gui.Tr.SLocalize("AlreadyCheckedOutBranch"))
branch := gui.getSelectedBranch(v)
if err := gui.GitCommand.Checkout(branch.Name, false); err != nil {
gui.createErrorPanel(g, err.Error())
}
branch := gui.getSelectedBranch()
return gui.handleCheckoutRef(branch.Name, handleCheckoutRefOptions{})
}
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.surfaceError(err)
}
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, true, title, message, func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.Checkout(branch.Name, commands.CheckoutOptions{Force: true}); err != nil {
_ = gui.surfaceError(err)
return gui.createConfirmationPanel(g, v, title, message, func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.Checkout(branch.Name, true); err != nil {
gui.createErrorPanel(g, err.Error())
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
return gui.refreshSidePanels(g)
}, nil)
}
type handleCheckoutRefOptions struct {
WaitingStatus string
EnvVars []string
}
func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions) error {
waitingStatus := options.WaitingStatus
if waitingStatus == "" {
waitingStatus = gui.Tr.SLocalize("CheckingOutStatus")
}
cmdOptions := commands.CheckoutOptions{Force: false, EnvVars: options.EnvVars}
onSuccess := func() {
gui.State.Panels.Branches.SelectedLine = 0
gui.State.Panels.Commits.SelectedLine = 0
// loading a heap of commits is slow so we limit them whenever doing a reset
gui.State.Panels.Commits.LimitCommits = true
}
return gui.WithWaitingStatus(waitingStatus, func() error {
if err := gui.GitCommand.Checkout(ref, cmdOptions); err != nil {
// note, this will only work for english-language git commands. If we force git to use english, and the error isn't this one, then the user will receive an english command they may not understand. I'm not sure what the best solution to this is. Running the command once in english and a second time in the native language is one option
if strings.Contains(err.Error(), "Please commit your changes or stash them before you switch branch") {
// offer to autostash changes
return gui.createConfirmationPanel(gui.g, gui.getBranchesView(), true, gui.Tr.SLocalize("AutoStashTitle"), gui.Tr.SLocalize("AutoStashPrompt"), func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.StashSave(gui.Tr.SLocalize("StashPrefix") + ref); err != nil {
return gui.surfaceError(err)
}
if err := gui.GitCommand.Checkout(ref, cmdOptions); err != nil {
return gui.surfaceError(err)
}
onSuccess()
if err := gui.GitCommand.StashDo(0, "pop"); err != nil {
if err := gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI}); err != nil {
return err
}
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI})
}, nil)
}
if err := gui.surfaceError(err); err != nil {
return err
}
}
onSuccess()
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI})
})
}
func (gui *Gui) handleCheckoutByName(g *gocui.Gui, v *gocui.View) error {
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("BranchName")+":", "", func(g *gocui.Gui, v *gocui.View) error {
return gui.handleCheckoutRef(gui.trimmedContent(v), handleCheckoutRefOptions{})
gui.createPromptPanel(g, v, gui.Tr.SLocalize("BranchName")+":", func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.Checkout(gui.trimmedContent(v), false); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return gui.refreshSidePanels(g)
})
}
func (gui *Gui) getCheckedOutBranch() *commands.Branch {
if len(gui.State.Branches) == 0 {
return nil
}
return gui.State.Branches[0]
return nil
}
func (gui *Gui) handleNewBranch(g *gocui.Gui, v *gocui.View) error {
branch := gui.getSelectedBranch()
if branch == nil {
return nil
}
branch := gui.State.Branches[0]
message := gui.Tr.TemplateLocalize(
"NewBranchNameBranchOff",
Teml{
"branchName": branch.Name,
},
)
return gui.createPromptPanel(g, v, message, "", func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.NewBranch(gui.trimmedContent(v), branch.Name); err != nil {
return gui.surfaceError(err)
gui.createPromptPanel(g, v, message, func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.NewBranch(gui.trimmedContent(v)); err != nil {
return gui.createErrorPanel(g, err.Error())
}
gui.State.Panels.Branches.SelectedLine = 0
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
gui.refreshSidePanels(g)
return gui.handleBranchSelect(g, v)
})
return nil
}
func (gui *Gui) handleDeleteBranch(g *gocui.Gui, v *gocui.View) error {
return gui.deleteBranch(g, v, false)
}
func (gui *Gui) deleteBranch(g *gocui.Gui, v *gocui.View, force bool) error {
selectedBranch := gui.getSelectedBranch()
if selectedBranch == nil {
return nil
}
checkedOutBranch := gui.getCheckedOutBranch()
checkedOutBranch := gui.State.Branches[0]
selectedBranch := gui.getSelectedBranch(v)
if checkedOutBranch.Name == selectedBranch.Name {
return gui.createErrorPanel(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"
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantDeleteCheckOutBranch"))
}
message := gui.Tr.TemplateLocalize(
messageID,
"DeleteBranchMessage",
Teml{
"selectedBranchName": selectedBranch.Name,
},
)
return gui.createConfirmationPanel(g, v, true, title, message, func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.DeleteBranch(selectedBranch.Name, force); err != nil {
errMessage := err.Error()
if !force && strings.Contains(errMessage, "is not fully merged") {
return gui.deleteNamedBranch(g, v, selectedBranch, true)
}
return gui.createErrorPanel(errMessage)
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); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{BRANCHES}})
return gui.refreshSidePanels(g)
}, nil)
}
func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
if gui.GitCommand.IsHeadDetached() {
return gui.createErrorPanel("Cannot merge branch in detached head state. You might have checked out a commit directly or a remote branch, in which case you should checkout the local branch you want to be on")
}
checkedOutBranchName := gui.getCheckedOutBranch().Name
if checkedOutBranchName == branchName {
return gui.createErrorPanel(gui.Tr.SLocalize("CantMergeBranchIntoItself"))
}
prompt := gui.Tr.TemplateLocalize(
"ConfirmMerge",
Teml{
"checkedOutBranch": checkedOutBranchName,
"selectedBranch": branchName,
},
)
return gui.createConfirmationPanel(gui.g, gui.getBranchesView(), true, gui.Tr.SLocalize("MergingTitle"), prompt,
func(g *gocui.Gui, v *gocui.View) error {
err := gui.GitCommand.Merge(branchName)
return gui.handleGenericMergeCommandResult(err)
}, nil)
}
func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
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"))
}
if err := gui.GitCommand.Merge(selectedBranch.Name); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return nil
}
func (gui *Gui) getSelectedBranch(v *gocui.View) commands.Branch {
lineNumber := gui.getItemPosition(v)
return gui.State.Branches[lineNumber]
}
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
}
selectedBranchName := gui.getSelectedBranch().Name
return gui.mergeBranchIntoCheckedOutBranch(selectedBranchName)
}
func (gui *Gui) handleRebaseOntoLocalBranch(g *gocui.Gui, v *gocui.View) error {
selectedBranchName := gui.getSelectedBranch().Name
return gui.handleRebaseOntoBranch(selectedBranchName)
}
func (gui *Gui) handleRebaseOntoBranch(selectedBranchName string) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
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"))
}
checkedOutBranch := gui.getCheckedOutBranch().Name
if selectedBranchName == checkedOutBranch {
return gui.createErrorPanel(gui.Tr.SLocalize("CantRebaseOntoSelf"))
}
prompt := gui.Tr.TemplateLocalize(
"ConfirmRebase",
Teml{
"checkedOutBranch": checkedOutBranch,
"selectedBranch": selectedBranchName,
},
)
return gui.createConfirmationPanel(gui.g, gui.getBranchesView(), true, gui.Tr.SLocalize("RebasingTitle"), prompt,
func(g *gocui.Gui, v *gocui.View) error {
err := gui.GitCommand.RebaseBranch(selectedBranchName)
return gui.handleGenericMergeCommandResult(err)
}, nil)
}
func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
branch := gui.getSelectedBranch()
if branch == nil {
return nil
}
if branch.Pushables == "" {
return nil
}
if branch.Pushables == "?" {
return gui.createErrorPanel(gui.Tr.SLocalize("FwdNoUpstream"))
}
if branch.Pushables != "0" {
return gui.createErrorPanel(gui.Tr.SLocalize("FwdCommitsToPush"))
}
upstream, err := gui.GitCommand.GetUpstreamForBranch(branch.Name)
if err != nil {
return gui.surfaceError(err)
}
split := strings.Split(upstream, "/")
remoteName := split[0]
remoteBranchName := strings.Join(split[1:], "/")
message := gui.Tr.TemplateLocalize(
"Fetching",
Teml{
"from": fmt.Sprintf("%s/%s", remoteName, remoteBranchName),
"to": branch.Name,
},
)
go func() {
_ = gui.createLoaderPanel(gui.g, v, message)
if gui.State.Panels.Branches.SelectedLine == 0 {
if err := gui.GitCommand.PullWithoutPasswordCheck("--ff-only"); err != nil {
_ = gui.surfaceError(err)
return
}
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC})
} else {
if err := gui.GitCommand.FastForward(branch.Name, remoteName, remoteBranchName); err != nil {
_ = gui.surfaceError(err)
return
}
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{BRANCHES}})
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.closeConfirmationPrompt(gui.g, true)
gui.renderString(g, "main", diff)
}()
return nil
}
func (gui *Gui) onBranchesTabClick(tabIndex int) error {
contexts := []string{"local-branches", "remotes", "tags"}
branchesView := gui.getBranchesView()
branchesView.TabIndex = tabIndex
return gui.switchBranchesPanelContext(contexts[tabIndex])
}
func (gui *Gui) switchBranchesPanelContext(context string) error {
branchesView := gui.getBranchesView()
branchesView.Context = context
if err := gui.onSearchEscape(); err != nil {
return err
}
contextTabIndexMap := map[string]int{
"local-branches": 0,
"remotes": 1,
"remote-branches": 1,
"tags": 2,
}
branchesView.TabIndex = contextTabIndexMap[context]
return gui.refreshBranchesViewWithSelection()
}
func (gui *Gui) refreshBranchesViewWithSelection() error {
branchesView := gui.getBranchesView()
switch branchesView.Context {
case "local-branches":
return gui.renderLocalBranchesWithSelection()
case "remotes":
return gui.renderRemotesWithSelection()
case "remote-branches":
return gui.renderRemoteBranchesWithSelection()
case "tags":
return gui.renderTagsWithSelection()
}
// 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
}
func (gui *Gui) handleNextBranchesTab(g *gocui.Gui, v *gocui.View) error {
return gui.onBranchesTabClick(
utils.ModuloWithWrap(v.TabIndex+1, len(v.Tabs)),
)
}
func (gui *Gui) handlePrevBranchesTab(g *gocui.Gui, v *gocui.View) error {
return gui.onBranchesTabClick(
utils.ModuloWithWrap(v.TabIndex-1, len(v.Tabs)),
)
}
func (gui *Gui) handleCreateResetToBranchMenu(g *gocui.Gui, v *gocui.View) error {
branch := gui.getSelectedBranch()
if branch == nil {
return nil
}
return gui.createResetMenu(branch.Name)
}
func (gui *Gui) onBranchesPanelSearchSelect(selectedLine int) error {
branchesView := gui.getBranchesView()
switch branchesView.Context {
case "local-branches":
gui.State.Panels.Branches.SelectedLine = selectedLine
return gui.handleBranchSelect(gui.g, branchesView)
case "remotes":
gui.State.Panels.Remotes.SelectedLine = selectedLine
return gui.handleRemoteSelect(gui.g, branchesView)
case "remote-branches":
gui.State.Panels.RemoteBranches.SelectedLine = selectedLine
return gui.handleRemoteBranchSelect(gui.g, branchesView)
}
return nil
}
func (gui *Gui) handleRenameBranch(g *gocui.Gui, v *gocui.View) error {
branch := gui.getSelectedBranch()
if branch == nil {
return nil
}
// TODO: find a way to not checkout the branch here if it's not the current branch (i.e. find some
// way to get it to show up in the reflog)
promptForNewName := func() error {
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("NewBranchNamePrompt")+" "+branch.Name+":", "", func(g *gocui.Gui, v *gocui.View) error {
newName := gui.trimmedContent(v)
if err := gui.GitCommand.RenameBranch(branch.Name, newName); err != nil {
return gui.surfaceError(err)
}
// need to checkout so that the branch shows up in our reflog and therefore
// doesn't get lost among all the other branches when we switch to something else
if err := gui.GitCommand.Checkout(newName, commands.CheckoutOptions{Force: false}); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
})
}
// I could do an explicit check here for whether the branch is tracking a remote branch
// but if we've selected it we'll already know that via Pullables and Pullables.
// Bit of a hack but I'm lazy.
notTrackingRemote := branch.Pullables == "?"
if notTrackingRemote {
return promptForNewName()
}
return gui.createConfirmationPanel(gui.g, v, true, gui.Tr.SLocalize("renameBranch"), gui.Tr.SLocalize("RenameBranchWarning"), func(_g *gocui.Gui, _v *gocui.View) error {
return promptForNewName()
}, nil)
}
func (gui *Gui) currentBranch() *commands.Branch {
if len(gui.State.Branches) == 0 {
return nil
}
return gui.State.Branches[0]
}
func (gui *Gui) handleClipboardCopyBranch(g *gocui.Gui, v *gocui.View) error {
branch := gui.getSelectedBranch()
if branch == nil {
return nil
}
return gui.OSCommand.CopyToClipboard(branch.Name)
}
-223
View File
@@ -1,223 +0,0 @@
package gui
import (
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
)
func (gui *Gui) getSelectedCommitFile() *commands.CommitFile {
selectedLine := gui.State.Panels.CommitFiles.SelectedLine
if selectedLine == -1 {
return nil
}
return gui.State.CommitFiles[selectedLine]
}
func (gui *Gui) handleCommitFilesClick(g *gocui.Gui, v *gocui.View) error {
itemCount := len(gui.State.CommitFiles)
handleSelect := gui.handleCommitFileSelect
selectedLine := &gui.State.Panels.CommitFiles.SelectedLine
return gui.handleClick(v, itemCount, selectedLine, handleSelect)
}
func (gui *Gui) handleCommitFileSelect(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
gui.getMainView().Title = "Patch"
if gui.currentViewName() == "commitFiles" {
gui.handleEscapeLineByLinePanel()
}
commitFile := gui.getSelectedCommitFile()
if commitFile == nil {
gui.renderString(g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
return nil
}
if err := gui.refreshSecondaryPatchPanel(); err != nil {
return err
}
v.FocusPoint(0, gui.State.Panels.CommitFiles.SelectedLine)
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowCommitFileCmdStr(commitFile.Sha, commitFile.Name, false),
)
if err := gui.newPtyTask("main", cmd); err != nil {
gui.Log.Error(err)
}
return nil
}
func (gui *Gui) handleSwitchToCommitsPanel(g *gocui.Gui, v *gocui.View) error {
return gui.switchFocus(g, v, gui.getCommitsView())
}
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.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}
func (gui *Gui) handleDiscardOldFileChange(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
return err
}
fileName := gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLine].Name
return gui.createConfirmationPanel(gui.g, v, true, gui.Tr.SLocalize("DiscardFileChangesTitle"), gui.Tr.SLocalize("DiscardFileChangesPrompt"), func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
if err := gui.GitCommand.DiscardOldFileChanges(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, fileName); err != nil {
if err := gui.handleGenericMergeCommandResult(err); err != nil {
return err
}
}
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI})
})
}, nil)
}
func (gui *Gui) refreshCommitFilesView() error {
if err := gui.refreshSecondaryPatchPanel(); err != nil {
return err
}
if err := gui.refreshPatchBuildingPanel(-1); err != nil {
return err
}
commit := gui.getSelectedCommit()
if commit == nil {
return nil
}
files, err := gui.GitCommand.GetCommitFiles(commit.Sha, gui.GitCommand.PatchManager)
if err != nil {
return gui.surfaceError(err)
}
gui.State.CommitFiles = files
gui.refreshSelectedLine(&gui.State.Panels.CommitFiles.SelectedLine, len(gui.State.CommitFiles))
commitsFileView := gui.getCommitFilesView()
displayStrings := presentation.GetCommitFileListDisplayStrings(gui.State.CommitFiles, gui.State.Diff.Ref)
gui.renderDisplayStrings(commitsFileView, displayStrings)
return gui.handleCommitFileSelect(gui.g, commitsFileView)
}
func (gui *Gui) handleOpenOldCommitFile(g *gocui.Gui, v *gocui.View) error {
file := gui.getSelectedCommitFile()
return gui.openFile(file.Name)
}
func (gui *Gui) handleToggleFileForPatch(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
return err
}
commitFile := gui.getSelectedCommitFile()
if commitFile == nil {
gui.renderString(g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
return nil
}
toggleTheFile := func() error {
if !gui.GitCommand.PatchManager.CommitSelected() {
if err := gui.startPatchManager(); err != nil {
return err
}
}
gui.GitCommand.PatchManager.ToggleFileWhole(commitFile.Name)
return gui.refreshCommitFilesView()
}
if gui.GitCommand.PatchManager.CommitSelected() && gui.GitCommand.PatchManager.CommitSha != commitFile.Sha {
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("DiscardPatch"), gui.Tr.SLocalize("DiscardPatchConfirm"), func(g *gocui.Gui, v *gocui.View) error {
gui.GitCommand.PatchManager.Reset()
return toggleTheFile()
}, nil)
}
return toggleTheFile()
}
func (gui *Gui) startPatchManager() error {
diffMap := map[string]string{}
for _, commitFile := range gui.State.CommitFiles {
commitText, err := gui.GitCommand.ShowCommitFile(commitFile.Sha, commitFile.Name, true)
if err != nil {
return err
}
diffMap[commitFile.Name] = commitText
}
commit := gui.getSelectedCommit()
if commit == nil {
return errors.New("No commit selected")
}
gui.GitCommand.PatchManager.Start(commit.Sha, diffMap)
return nil
}
func (gui *Gui) handleEnterCommitFile(g *gocui.Gui, v *gocui.View) error {
return gui.enterCommitFile(-1)
}
func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
return err
}
commitFile := gui.getSelectedCommitFile()
if commitFile == nil {
gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
return nil
}
enterTheFile := func(selectedLineIdx int) error {
if !gui.GitCommand.PatchManager.CommitSelected() {
if err := gui.startPatchManager(); err != nil {
return err
}
}
gui.changeMainViewsContext("patch-building")
if err := gui.switchFocus(gui.g, gui.getCommitFilesView(), gui.getMainView()); err != nil {
return err
}
return gui.refreshPatchBuildingPanel(selectedLineIdx)
}
if gui.GitCommand.PatchManager.CommitSelected() && gui.GitCommand.PatchManager.CommitSha != commitFile.Sha {
return gui.createConfirmationPanel(gui.g, gui.getCommitFilesView(), false, gui.Tr.SLocalize("DiscardPatch"), gui.Tr.SLocalize("DiscardPatchConfirm"), func(g *gocui.Gui, v *gocui.View) error {
gui.GitCommand.PatchManager.Reset()
return enterTheFile(selectedLineIdx)
}, func(g *gocui.Gui, v *gocui.View) error {
return gui.switchFocus(gui.g, nil, gui.getCommitFilesView())
})
}
return enterTheFile(selectedLineIdx)
}
func (gui *Gui) onCommitFilesPanelSearchSelect(selectedLine int) error {
gui.State.Panels.CommitFiles.SelectedLine = selectedLine
return gui.handleCommitFileSelect(gui.g, gui.getCommitFilesView())
}
+31 -93
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.surfaceError(err)
}
}
if sub != nil {
gui.SubProcess = sub
return false, gui.Errors.ErrSubProcess
}
return true, nil
}
func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
message := gui.trimmedContent(v)
if message == "" {
return gui.createErrorPanel(gui.Tr.SLocalize("CommitWithoutMessageErr"))
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(refreshOptions{mode: ASYNC})
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())
g.SetViewOnBottom("commitMessage")
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{
@@ -68,54 +55,5 @@ func (gui *Gui) handleCommitFocused(g *gocui.Gui, v *gocui.View) error {
"keyBindConfirm": "enter",
},
)
gui.renderString(g, "options", message)
return nil
}
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)
}
// we've just copy+pasted the editor from gocui to here so that we can also re-
// render the commit message length on each keypress
func (gui *Gui) commitMessageEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
switch {
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
v.EditDelete(true)
case key == gocui.KeyDelete:
v.EditDelete(false)
case key == gocui.KeyArrowDown:
v.MoveCursor(0, 1, false)
case key == gocui.KeyArrowUp:
v.MoveCursor(0, -1, false)
case key == gocui.KeyArrowLeft:
v.MoveCursor(-1, 0, false)
case key == gocui.KeyArrowRight:
v.MoveCursor(1, 0, false)
case key == gocui.KeyTab:
v.EditNewLine()
case key == gocui.KeySpace:
v.EditWrite(' ')
case key == gocui.KeyInsert:
v.Overwrite = !v.Overwrite
case key == gocui.KeyCtrlU:
v.EditDeleteToStartOfLine()
case key == gocui.KeyCtrlA:
v.EditGotoToStartOfLine()
case key == gocui.KeyCtrlE:
v.EditGotoToEndOfLine()
default:
v.EditWrite(ch)
}
gui.RenderCommitLength()
return gui.renderString(g, "options", message)
}
+119 -661
View File
@@ -1,714 +1,172 @@
package gui
import (
"strconv"
"sync"
"errors"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// list panel functions
func (gui *Gui) getSelectedCommit() *commands.Commit {
selectedLine := gui.State.Panels.Commits.SelectedLine
if selectedLine == -1 {
func (gui *Gui) refreshCommits(g *gocui.Gui) error {
g.Update(func(*gocui.Gui) error {
gui.State.Commits = gui.GitCommand.GetCommits()
v, err := g.View("commits")
if err != nil {
panic(err)
}
v.Clear()
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)
}
gui.refreshStatus(g)
if g.CurrentView().Name() == "commits" {
gui.handleCommitSelect(g, v)
}
return nil
}
})
return nil
}
return gui.State.Commits[selectedLine]
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, err := gui.getSelectedCommit(g)
if err != nil {
panic(err)
}
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(g); err != nil {
panic(err)
}
gui.resetOrigin(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 gui.popupPanelFocused() {
return nil
}
// this probably belongs in an 'onFocus' function than a 'commit selected' function
if err := gui.refreshSecondaryPatchPanel(); err != nil {
if err := gui.renderCommitsOptions(g); err != nil {
return err
}
if _, err := gui.g.SetCurrentView(v.Name()); err != nil {
return err
}
state := gui.State.Panels.Commits
if state.SelectedLine > 290 && state.LimitCommits {
state.LimitCommits = false
go func() {
if err := gui.refreshCommitsWithLimit(); err != nil {
_ = gui.surfaceError(err)
}
}()
}
gui.getMainView().Title = "Patch"
gui.getSecondaryView().Title = "Custom Patch"
gui.handleEscapeLineByLinePanel()
commit := gui.getSelectedCommit()
if commit == nil {
return gui.newStringTask("main", gui.Tr.SLocalize("NoCommitsThisBranch"))
}
v.FocusPoint(0, gui.State.Panels.Commits.SelectedLine)
if gui.inDiffMode() {
return gui.renderDiff()
}
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.FilterPath),
)
if err := gui.newPtyTask("main", cmd); err != nil {
gui.Log.Error(err)
}
return nil
}
// during startup, the bottleneck is fetching the reflog entries. We need these
// on startup to sort the branches by recency. So we have two phases: INITIAL, and COMPLETE.
// In the initial phase we don't get any reflog commits, but we asynchronously get them
// and refresh the branches after that
func (gui *Gui) refreshReflogCommitsConsideringStartup() {
switch gui.State.StartupStage {
case INITIAL:
go func() {
_ = gui.refreshReflogCommits()
gui.refreshBranches()
gui.State.StartupStage = COMPLETE
}()
case COMPLETE:
_ = gui.refreshReflogCommits()
}
}
// whenever we change commits, we should update branches because the upstream/downstream
// counts can change. Whenever we change branches we should probably also change commits
// e.g. in the case of switching branches.
func (gui *Gui) refreshCommits() error {
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
gui.refreshReflogCommitsConsideringStartup()
gui.refreshBranches()
wg.Done()
}()
go func() {
_ = gui.refreshCommitsWithLimit()
if gui.g.CurrentView() == gui.getCommitFilesView() || (gui.g.CurrentView() == gui.getMainView() && gui.State.MainContext == "patch-building") {
_ = gui.refreshCommitFilesView()
}
wg.Done()
}()
wg.Wait()
return nil
}
func (gui *Gui) refreshCommitsWithLimit() error {
builder, err := commands.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr, gui.State.CherryPickedCommits)
commit, err := gui.getSelectedCommit(g)
if err != nil {
return err
}
commits, err := builder.GetCommits(commands.GetCommitsOptions{Limit: gui.State.Panels.Commits.LimitCommits, FilterPath: gui.State.FilterPath})
if err != nil {
return err
}
gui.State.Commits = commits
if gui.getCommitsView().Context == "branch-commits" {
if err := gui.renderBranchCommitsWithSelection(); err != nil {
if err != errors.New(gui.Tr.SLocalize("NoCommitsThisBranch")) {
return err
}
return gui.renderString(g, "main", gui.Tr.SLocalize("NoCommitsThisBranch"))
}
return nil
commitText := gui.GitCommand.Show(commit.Sha)
return gui.renderString(g, "main", commitText)
}
// specific functions
func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
if gui.getItemPosition(v) != 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlySquashTopmostCommit"))
}
if len(gui.State.Commits) <= 1 {
return gui.createErrorPanel(gui.Tr.SLocalize("YouNoCommitsToSquash"))
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())
}
if err := gui.refreshCommits(g); err != nil {
panic(err)
}
gui.refreshStatus(g)
return gui.handleCommitSelect(g, v)
}
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("Squash"), gui.Tr.SLocalize("SureSquashThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("SquashingStatus"), func() error {
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "squash")
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
// TODO: move to files panel
func (gui *Gui) anyUnStagedChanges(files []commands.File) bool {
for _, file := range files {
if file.Tracked && file.HasUnstagedChanges {
return true
}
}
return false
}
func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
if len(gui.State.Commits) == 1 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash"))
}
if len(gui.State.Commits) <= 1 {
return gui.createErrorPanel(gui.Tr.SLocalize("YouNoCommitsToSquash"))
if gui.anyUnStagedChanges(gui.State.Files) {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantFixupWhileUnstagedChanges"))
}
applied, err := gui.handleMidRebaseCommand("fixup")
branch := gui.State.Branches[0]
commit, err := gui.getSelectedCommit(g)
if err != nil {
return err
}
if applied {
return nil
}
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("Fixup"), gui.Tr.SLocalize("SureFixupThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("FixingStatus"), func() error {
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "fixup")
return gui.handleGenericMergeCommandResult(err)
})
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 {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
if gui.getItemPosition(v) != 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlyRenameTopCommit"))
}
applied, err := gui.handleMidRebaseCommand("reword")
if err != nil {
return err
}
if applied {
return nil
}
if gui.State.Panels.Commits.SelectedLine != 0 {
return gui.createErrorPanel(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.surfaceError(err)
return gui.createErrorPanel(g, err.Error())
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
if err := gui.refreshCommits(g); err != nil {
panic(err)
}
return gui.handleCommitSelect(g, v)
})
}
func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
applied, err := gui.handleMidRebaseCommand("reword")
if err != nil {
return err
}
if applied {
return nil
}
subProcess, err := gui.GitCommand.RewordCommit(gui.State.Commits, gui.State.Panels.Commits.SelectedLine)
if err != nil {
return gui.surfaceError(err)
}
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.Tr.SLocalize("rewordNotSupported"))
}
if err := gui.GitCommand.EditRebaseTodo(gui.State.Panels.Commits.SelectedLine, action); err != nil {
return false, gui.surfaceError(err)
}
// TODO: consider doing this in a way that is less expensive. We don't actually
// need to reload all the commits, just the TODO commits.
return true, gui.refreshSidePanels(refreshOptions{scope: []int{COMMITS}})
}
func (gui *Gui) handleCommitDelete(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
applied, err := gui.handleMidRebaseCommand("drop")
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, true, gui.Tr.SLocalize("DeleteCommitTitle"), gui.Tr.SLocalize("DeleteCommitPrompt"), func(*gocui.Gui, *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("DeletingStatus"), func() error {
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, "drop")
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
}
func (gui *Gui) handleCommitMoveDown(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
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
}
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.surfaceError(err)
}
gui.State.Panels.Commits.SelectedLine++
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI, scope: []int{COMMITS, BRANCHES}})
}
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 {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
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.surfaceError(err)
}
gui.State.Panels.Commits.SelectedLine--
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI, scope: []int{COMMITS, BRANCHES}})
}
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 {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
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 {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
return gui.createConfirmationPanel(gui.g, v, true, gui.Tr.SLocalize("AmendCommitTitle"), gui.Tr.SLocalize("AmendCommitPrompt"), func(*gocui.Gui, *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("AmendingStatus"), func() error {
err := gui.GitCommand.AmendTo(gui.State.Commits[gui.State.Panels.Commits.SelectedLine].Sha)
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
}
func (gui *Gui) handleCommitPick(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
applied, err := gui.handleMidRebaseCommand("pick")
if err != nil {
return err
}
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.handlePullFiles(g, v)
}
func (gui *Gui) handleCommitRevert(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
if err := gui.GitCommand.Revert(gui.State.Commits[gui.State.Panels.Commits.SelectedLine].Sha); err != nil {
return gui.surfaceError(err)
}
gui.State.Panels.Commits.SelectedLine++
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI, scope: []int{COMMITS, BRANCHES}})
}
func (gui *Gui) handleCopyCommit(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
// get currently selected commit, add the sha to state.
commit := gui.State.Commits[gui.State.Panels.Commits.SelectedLine]
// 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.renderBranchCommitsWithSelection()
}
}
gui.addCommitToCherryPickedCommits(gui.State.Panels.Commits.SelectedLine)
return gui.renderBranchCommitsWithSelection()
}
func (gui *Gui) cherryPickedCommitShaMap() map[string]bool {
commitShaMap := map[string]bool{}
for _, commit := range gui.State.CherryPickedCommits {
commitShaMap[commit.Sha] = true
}
return commitShaMap
}
func (gui *Gui) addCommitToCherryPickedCommits(index int) {
commitShaMap := gui.cherryPickedCommitShaMap()
commitShaMap[gui.State.Commits[index].Sha] = true
newCommits := []*commands.Commit{}
for _, commit := range gui.State.Commits {
if commitShaMap[commit.Sha] {
// duplicating just the things we need to put in the rebase TODO list
newCommits = append(newCommits, &commands.Commit{Name: commit.Name, Sha: commit.Sha})
}
}
gui.State.CherryPickedCommits = newCommits
}
func (gui *Gui) handleCopyCommitRange(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
commitShaMap := gui.cherryPickedCommitShaMap()
// find the last commit that is copied that's above our position
// if there are none, startIndex = 0
startIndex := 0
for index, commit := range gui.State.Commits[0:gui.State.Panels.Commits.SelectedLine] {
if commitShaMap[commit.Sha] {
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.renderBranchCommitsWithSelection()
}
// HandlePasteCommits begins a cherry-pick rebase with the commits the user has copied
func (gui *Gui) HandlePasteCommits(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("CherryPick"), gui.Tr.SLocalize("SureCherryPick"), func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("CherryPickingStatus"), func() error {
err := gui.GitCommand.CherryPickCommits(gui.State.CherryPickedCommits)
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
}
func (gui *Gui) handleSwitchToCommitFilesPanel(g *gocui.Gui, v *gocui.View) error {
if err := gui.refreshCommitFilesView(); err != nil {
return err
}
return gui.switchFocus(g, gui.getCommitsView(), gui.getCommitFilesView())
}
func (gui *Gui) 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 {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
commit := gui.getSelectedCommit()
if commit == nil {
return nil
}
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("CreateFixupCommit"), gui.Tr.TemplateLocalize(
"SureCreateFixupCommit",
Teml{
"commit": commit.Sha,
},
), func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.CreateFixupCommit(commit.Sha); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}, nil)
}
func (gui *Gui) handleSquashAllAboveFixupCommits(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
commit := gui.getSelectedCommit()
if commit == nil {
return nil
}
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("SquashAboveCommits"), gui.Tr.TemplateLocalize(
"SureSquashAboveCommits",
Teml{
"commit": commit.Sha,
},
), func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("SquashingStatus"), func() error {
err := gui.GitCommand.SquashAllAboveFixupCommits(commit.Sha)
return gui.handleGenericMergeCommandResult(err)
})
}, nil)
}
func (gui *Gui) handleTagCommit(g *gocui.Gui, v *gocui.View) error {
// TODO: bring up menu asking if you want to make a lightweight or annotated tag
// if annotated, switch to a subprocess to create the message
commit := gui.getSelectedCommit()
if commit == nil {
return nil
}
return gui.handleCreateLightweightTag(commit.Sha)
}
func (gui *Gui) handleCreateLightweightTag(commitSha string) error {
return gui.createPromptPanel(gui.g, gui.getCommitsView(), gui.Tr.SLocalize("TagNameTitle"), "", func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.CreateLightweightTag(v.Buffer(), commitSha); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{COMMITS, TAGS}})
})
}
func (gui *Gui) handleCheckoutCommit(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedCommit()
if commit == nil {
return nil
}
return gui.createConfirmationPanel(g, gui.getCommitsView(), true, gui.Tr.SLocalize("checkoutCommit"), gui.Tr.SLocalize("SureCheckoutThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{})
}, nil)
}
func (gui *Gui) renderBranchCommitsWithSelection() error {
commitsView := gui.getCommitsView()
gui.refreshSelectedLine(&gui.State.Panels.Commits.SelectedLine, len(gui.State.Commits))
displayStrings := presentation.GetCommitListDisplayStrings(gui.State.Commits, gui.State.ScreenMode != SCREEN_NORMAL, gui.cherryPickedCommitShaMap(), gui.State.Diff.Ref)
gui.renderDisplayStrings(commitsView, displayStrings)
if gui.g.CurrentView() == commitsView && commitsView.Context == "branch-commits" {
if err := gui.handleCommitSelect(gui.g, commitsView); err != nil {
return err
}
}
return nil
}
func (gui *Gui) onCommitsTabClick(tabIndex int) error {
contexts := []string{"branch-commits", "reflog-commits"}
commitsView := gui.getCommitsView()
commitsView.TabIndex = tabIndex
return gui.switchCommitsPanelContext(contexts[tabIndex])
}
func (gui *Gui) switchCommitsPanelContext(context string) error {
commitsView := gui.getCommitsView()
commitsView.Context = context
if err := gui.onSearchEscape(); err != nil {
return err
}
contextTabIndexMap := map[string]int{
"branch-commits": 0,
"reflog-commits": 1,
}
commitsView.TabIndex = contextTabIndexMap[context]
return gui.refreshCommitsViewWithSelection()
}
func (gui *Gui) refreshCommitsViewWithSelection() error {
commitsView := gui.getCommitsView()
switch commitsView.Context {
case "branch-commits":
return gui.renderBranchCommitsWithSelection()
case "reflog-commits":
return gui.renderReflogCommitsWithSelection()
}
return nil
}
func (gui *Gui) handleNextCommitsTab(g *gocui.Gui, v *gocui.View) error {
return gui.onCommitsTabClick(
utils.ModuloWithWrap(v.TabIndex+1, len(v.Tabs)),
)
}
func (gui *Gui) handlePrevCommitsTab(g *gocui.Gui, v *gocui.View) error {
return gui.onCommitsTabClick(
utils.ModuloWithWrap(v.TabIndex-1, len(v.Tabs)),
)
}
func (gui *Gui) handleCreateCommitResetMenu(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedCommit()
if commit == nil {
return gui.createErrorPanel(gui.Tr.SLocalize("NoCommitsThisBranch"))
}
return gui.createResetMenu(commit.Sha)
}
func (gui *Gui) onCommitsPanelSearchSelect(selectedLine int) error {
commitsView := gui.getCommitsView()
switch commitsView.Context {
case "branch-commits":
gui.State.Panels.Commits.SelectedLine = selectedLine
return gui.handleCommitSelect(gui.g, commitsView)
case "reflog-commits":
gui.State.Panels.ReflogCommits.SelectedLine = selectedLine
return gui.handleReflogCommitSelect(gui.g, commitsView)
}
return nil
}
func (gui *Gui) handleOpenSearchForCommitsPanel(g *gocui.Gui, v *gocui.View) error {
// we usually lazyload these commits but now that we're searching we need to load them now
if gui.State.Panels.Commits.LimitCommits {
gui.State.Panels.Commits.LimitCommits = false
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{COMMITS}}); err != nil {
return err
}
}
return gui.handleOpenSearch(gui.g, v)
}
func (gui *Gui) handleResetCherryPick(g *gocui.Gui, v *gocui.View) error {
gui.State.CherryPickedCommits = []*commands.Commit{}
return gui.renderBranchCommitsWithSelection()
}
func (gui *Gui) handleGotoBottomForCommitsPanel(g *gocui.Gui, v *gocui.View) error {
// we usually lazyload these commits but now that we're searching we need to load them now
if gui.State.Panels.Commits.LimitCommits {
gui.State.Panels.Commits.LimitCommits = false
if err := gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []int{COMMITS}}); err != nil {
return err
}
}
for _, view := range gui.getListViews() {
if view.viewName == "commits" {
return view.handleGotoBottom(g, v)
}
}
return nil
}
func (gui *Gui) handleClipboardCopyCommit(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedCommit()
if commit == nil {
return nil
}
return gui.OSCommand.CopyToClipboard(commit.Sha)
return gui.State.Commits[lineNumber], nil
}
+86 -114
View File
@@ -8,144 +8,122 @@ 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, returnFocusOnClose bool) func(*gocui.Gui, *gocui.View) error {
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, returnFocusOnClose)
return gui.closeConfirmationPrompt(g)
}
}
func (gui *Gui) closeConfirmationPrompt(g *gocui.Gui, returnFocusOnClose bool) error {
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)
}
view.Editable = false
if returnFocusOnClose {
if err := gui.returnFocus(g, view); err != nil {
panic(err)
}
if err := gui.returnFocus(g, view); err != nil {
panic(err)
}
g.DeleteKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone)
g.DeleteKeybinding("confirmation", gocui.KeyEsc, gocui.ModNone)
g.DeleteKeybindings("confirmation")
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 := 4 * width / 7
panelHeight := gui.getMessageHeight(wrap, prompt, panelWidth)
if panelHeight > height*3/4 {
panelHeight = height * 3 / 4
}
panelWidth := width / 2
panelHeight := gui.getMessageHeight(prompt, panelWidth)
return width/2 - panelWidth/2,
height/2 - panelHeight/2 - panelHeight%2 - 1,
width/2 + panelWidth/2,
height/2 + panelHeight/2
}
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
}
confirmationView.HasLoader = hasLoader
if hasLoader {
gui.g.StartTicking()
func (gui *Gui) createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, handleConfirm func(*gocui.Gui, *gocui.View) error) error {
gui.onNewPopupPanel()
// 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.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) createPopupPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, hasLoader bool, returnFocusOnClose bool, editable bool, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
func (gui *Gui) createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
gui.onNewPopupPanel()
g.Update(func(g *gocui.Gui) error {
// delete the existing confirmation panel if it exists
if view, _ := g.View("confirmation"); view != nil {
if err := gui.closeConfirmationPrompt(g, true); err != nil {
gui.Log.Error(err)
if err := gui.closeConfirmationPrompt(g); err != nil {
errMessage := gui.Tr.TemplateLocalize(
"CantCloseConfirmationPrompt",
Teml{
"error": err.Error(),
},
)
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 = editable
if editable {
go func() {
// TODO: remove this wait (right now if you remove it the EditGotoToEndOfLine method doesn't seem to work)
time.Sleep(time.Millisecond)
gui.g.Update(func(g *gocui.Gui) error {
confirmationView.EditGotoToEndOfLine()
return nil
})
}()
}
gui.renderString(g, "confirmation", prompt)
return gui.setKeyBindings(g, handleConfirm, handleClose, returnFocusOnClose)
return nil
})
return nil
}
func (gui *Gui) createLoaderPanel(g *gocui.Gui, currentView *gocui.View, prompt string) error {
return gui.createPopupPanel(g, currentView, "", prompt, true, true, false, nil, 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
}
// 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, returnFocusOnClose bool, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
return gui.createPopupPanel(g, currentView, title, prompt, false, returnFocusOnClose, false, handleConfirm, handleClose)
}
func (gui *Gui) createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, initialContent string, handleConfirm func(*gocui.Gui, *gocui.View) error) error {
return gui.createPopupPanel(gui.g, currentView, title, initialContent, false, true, true, handleConfirm, nil)
}
func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error, returnFocusOnClose bool) error {
func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
actions := gui.Tr.TemplateLocalize(
"CloseConfirm",
Teml{
@@ -153,43 +131,37 @@ func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*go
"keyBindConfirm": "enter",
},
)
gui.renderString(g, "options", actions)
if err := g.SetKeybinding("confirmation", nil, gocui.KeyEnter, gocui.ModNone, gui.wrappedConfirmationFunction(handleConfirm, returnFocusOnClose)); err != nil {
if err := g.SetKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone, gui.wrappedConfirmationFunction(handleConfirm)); err != nil {
return err
}
return g.SetKeybinding("confirmation", nil, gocui.KeyEsc, gocui.ModNone, gui.wrappedConfirmationFunction(handleClose, returnFocusOnClose))
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))
}
// 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)
}()
}
func (gui *Gui) createMessagePanel(g *gocui.Gui, currentView *gocui.View, title, prompt string) error {
return gui.createConfirmationPanel(g, currentView, title, prompt, nil, nil)
}
func (gui *Gui) createErrorPanel(g *gocui.Gui, message string) error {
currentView := g.CurrentView()
colorFunction := color.New(color.FgRed).SprintFunc()
coloredMessage := colorFunction(strings.TrimSpace(message))
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil {
return err
return gui.createConfirmationPanel(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
}
return gui.createConfirmationPanel(gui.g, nextView, true, gui.Tr.SLocalize("Error"), coloredMessage, nil, nil)
}
func (gui *Gui) createErrorPanel(message string) error {
return gui.createSpecificErrorPanel(message, gui.g.CurrentView(), true)
}
func (gui *Gui) surfaceError(err error) error {
return gui.createErrorPanel(err.Error())
gui.Log.Info(gui.Tr.SLocalize("resizingPopupPanel"))
_, err := g.SetView(v.Name(), x0, y0, x1, y1, 0)
return err
}
-19
View File
@@ -1,19 +0,0 @@
package gui
// changeContext is a helper function for when we want to change a 'main' context
// which currently just means a context that affects both the main and secondary views
// other views can have their context changed directly but this function helps
// keep the main and secondary views in sync
func (gui *Gui) changeMainViewsContext(context string) {
if gui.State.MainContext == context {
return
}
switch context {
case "normal", "patch-building", "staging", "merging":
gui.getMainView().Context = context
gui.getSecondaryView().Context = context
}
gui.State.MainContext = context
}
-95
View File
@@ -1,95 +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
v.Clear()
_ = v.SetCursor(0, 0)
_, _ = g.SetViewOnBottom("credentials")
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.refreshSidePanels(refreshOptions{})
}
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",
},
)
gui.renderString(g, "options", message)
return nil
}
// HandleCredentialsPopup handles the views after executing a command that might ask for credentials
func (gui *Gui) HandleCredentialsPopup(g *gocui.Gui, popupOpened bool, cmdErr error) {
if popupOpened {
_, _ = gui.g.SetViewOnBottom("credentials")
}
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, true)
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}
}
-181
View File
@@ -1,181 +0,0 @@
package gui
import (
"fmt"
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
)
func (gui *Gui) inDiffMode() bool {
return gui.State.Diff.Ref != ""
}
func (gui *Gui) exitDiffMode() error {
gui.State.Diff = DiffState{}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}
func (gui *Gui) renderDiff() error {
gui.getMainView().Title = "Diff"
gui.State.SplitMainPanel = false
filterArg := ""
if gui.inFilterMode() {
filterArg = fmt.Sprintf(" -- %s", gui.State.FilterPath)
}
cmd := gui.OSCommand.ExecutableFromString(
fmt.Sprintf("git diff --color %s %s", gui.diffStr(), filterArg),
)
if err := gui.newPtyTask("main", cmd); err != nil {
gui.Log.Error(err)
}
return nil
}
// currentDiffTerminals returns the current diff terminals of the currently selected item.
// in the case of a branch it returns both the branch and it's upstream name,
// which becomes an option when you bring up the diff menu, but when you're just
// flicking through branches it will be using the local branch name.
func (gui *Gui) currentDiffTerminals() []string {
currentView := gui.g.CurrentView()
if currentView == nil {
return nil
}
names := []string{}
switch currentView.Name() {
case "files":
// not supporting files for now
// file, err := gui.getSelectedFile()
// if err == nil {
// names = append(names, file.Name)
// }
case "commitFiles":
// not supporting commit files for now
// file := gui.getSelectedCommitFile()
// if file != nil {
// names = append(names, file.Name)
// }
case "commits":
var commit *commands.Commit
switch gui.getCommitsView().Context {
case "reflog-commits":
commit = gui.getSelectedReflogCommit()
case "branch-commits":
commit = gui.getSelectedCommit()
}
if commit != nil {
names = append(names, commit.Sha)
}
case "stash":
entry := gui.getSelectedStashEntry()
if entry != nil {
names = append(names, entry.RefName())
}
case "branches":
switch gui.getBranchesView().Context {
case "local-branches":
branch := gui.getSelectedBranch()
if branch != nil {
names = append(names, branch.Name)
if branch.UpstreamName != "" {
names = append(names, branch.UpstreamName)
}
}
case "remotes":
remote := gui.getSelectedRemote()
if remote != nil {
names = append(names, remote.Name)
}
case "remote-branches":
remoteBranch := gui.getSelectedRemoteBranch()
if remoteBranch != nil {
names = append(names, remoteBranch.FullName())
}
case "tags":
tag := gui.getSelectedTag()
if tag != nil {
names = append(names, tag.Name)
}
}
}
return names
}
func (gui *Gui) currentDiffTerminal() string {
names := gui.currentDiffTerminals()
if len(names) == 0 {
return ""
}
return names[0]
}
func (gui *Gui) diffStr() string {
output := gui.State.Diff.Ref
right := gui.currentDiffTerminal()
if right != "" {
output += " " + right
}
if gui.State.Diff.Reverse {
output += " -R"
}
return output
}
func (gui *Gui) handleCreateDiffingMenuPanel(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
names := gui.currentDiffTerminals()
menuItems := []*menuItem{}
for _, name := range names {
name := name
menuItems = append(menuItems, []*menuItem{
{
displayString: fmt.Sprintf("%s %s", gui.Tr.SLocalize("diff"), name),
onPress: func() error {
gui.State.Diff.Ref = name
// can scope this down based on current view but too lazy right now
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
},
},
}...)
}
menuItems = append(menuItems, []*menuItem{
{
displayString: gui.Tr.SLocalize("enterRefToDiff"),
onPress: func() error {
return gui.createPromptPanel(gui.g, v, gui.Tr.SLocalize("enteRefName"), "", func(g *gocui.Gui, promptView *gocui.View) error {
gui.State.Diff.Ref = strings.TrimSpace(promptView.Buffer())
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
})
},
},
}...)
if gui.inDiffMode() {
menuItems = append(menuItems, []*menuItem{
{
displayString: gui.Tr.SLocalize("swapDiff"),
onPress: func() error {
gui.State.Diff.Reverse = !gui.State.Diff.Reverse
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
},
},
{
displayString: gui.Tr.SLocalize("exitDiffMode"),
onPress: func() error {
gui.State.Diff = DiffState{}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
},
},
}...)
}
return gui.createMenu(gui.Tr.SLocalize("DiffingMenuTitle"), menuItems, createMenuOptions{showCancel: true})
}
-42
View File
@@ -1,42 +0,0 @@
package gui
import (
"github.com/jesseduffield/gocui"
)
func (gui *Gui) handleCreateDiscardMenu(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile()
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
}
return nil
}
menuItems := []*menuItem{
{
displayString: gui.Tr.SLocalize("discardAllChanges"),
onPress: func() error {
if err := gui.GitCommand.DiscardAllFileChanges(file); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
},
},
}
if file.HasStagedChanges && file.HasUnstagedChanges {
menuItems = append(menuItems, &menuItem{
displayString: gui.Tr.SLocalize("discardUnstagedChanges"),
onPress: func() error {
if err := gui.GitCommand.DiscardUnstagedFileChanges(file); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
},
})
}
return gui.createMenu(file.Name, menuItems, createMenuOptions{showCancel: true})
}
-146
View File
@@ -1,146 +0,0 @@
package gui
import (
"os"
"path/filepath"
"github.com/fsnotify/fsnotify"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/sirupsen/logrus"
)
// macs for some bizarre reason cap the number of watchable files to 256.
// there's no obvious platform agonstic way to check the situation of the user's
// computer so we're just arbitrarily capping at 200. This isn't so bad because
// file watching is only really an added bonus for faster refreshing.
const MAX_WATCHED_FILES = 50
type fileWatcher struct {
Watcher *fsnotify.Watcher
WatchedFilenames []string
Log *logrus.Entry
Disabled bool
}
func NewFileWatcher(log *logrus.Entry) *fileWatcher {
// TODO: get this going again, and ensure we don't see any crashes from it
return &fileWatcher{
Disabled: true,
}
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Error(err)
return &fileWatcher{
Disabled: true,
}
}
return &fileWatcher{
Watcher: watcher,
Log: log,
WatchedFilenames: make([]string, 0, MAX_WATCHED_FILES),
}
}
func (w *fileWatcher) watchingFilename(filename string) bool {
for _, watchedFilename := range w.WatchedFilenames {
if watchedFilename == filename {
return true
}
}
return false
}
func (w *fileWatcher) popOldestFilename() {
// shift the last off the array to make way for this one
oldestFilename := w.WatchedFilenames[0]
w.WatchedFilenames = w.WatchedFilenames[1:]
if err := w.Watcher.Remove(oldestFilename); err != nil {
// swallowing errors here because it doesn't really matter if we can't unwatch a file
w.Log.Warn(err)
}
}
func (w *fileWatcher) watchFilename(filename string) {
w.Log.Warn(filename)
if err := w.Watcher.Add(filename); err != nil {
// swallowing errors here because it doesn't really matter if we can't watch a file
w.Log.Warn(err)
}
// assume we're watching it now to be safe
w.WatchedFilenames = append(w.WatchedFilenames, filename)
}
func (w *fileWatcher) addFilesToFileWatcher(files []*commands.File) error {
if w.Disabled {
return nil
}
if len(files) == 0 {
return nil
}
// watch the files for changes
dirName, err := os.Getwd()
if err != nil {
return err
}
for _, file := range files[0:min(MAX_WATCHED_FILES, len(files))] {
if file.Deleted {
continue
}
filename := filepath.Join(dirName, file.Name)
if w.watchingFilename(filename) {
continue
}
if len(w.WatchedFilenames) > MAX_WATCHED_FILES {
w.popOldestFilename()
}
w.watchFilename(filename)
}
return nil
}
func min(a int, b int) int {
if a < b {
return a
}
return b
}
// NOTE: given that we often edit files ourselves, this may make us end up refreshing files too often
// TODO: consider watching the whole directory recursively (could be more expensive)
func (gui *Gui) watchFilesForChanges() {
gui.fileWatcher = NewFileWatcher(gui.Log)
if gui.fileWatcher.Disabled {
return
}
go func() {
for {
select {
// watch for events
case event := <-gui.fileWatcher.Watcher.Events:
if event.Op == fsnotify.Chmod {
// for some reason we pick up chmod events when they don't actually happen
continue
}
// only refresh if we're not already
if !gui.State.IsRefreshingFiles {
gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
}
// watch for errors
case err := <-gui.fileWatcher.Watcher.Errors:
if err != nil {
gui.Log.Warn(err)
}
}
}
}()
}
+272 -444
View File
@@ -7,125 +7,17 @@ import (
// "strings"
"fmt"
"regexp"
"os/exec"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// list panel functions
func (gui *Gui) getSelectedFile() (*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) selectFile(alreadySelected bool) error {
gui.getFilesView().FocusPoint(0, gui.State.Panels.Files.SelectedLine)
if gui.inDiffMode() {
return gui.renderDiff()
}
file, err := gui.getSelectedFile()
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
}
gui.State.SplitMainPanel = false
gui.getMainView().Title = ""
return gui.newStringTask("main", gui.Tr.SLocalize("NoChangedFiles"))
}
if !alreadySelected {
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
if err := gui.resetOrigin(gui.getSecondaryView()); err != nil {
return err
}
}
if file.HasInlineMergeConflicts {
gui.getMainView().Title = gui.Tr.SLocalize("MergeConflictsTitle")
gui.State.SplitMainPanel = false
return gui.refreshMergePanel()
}
if file.HasStagedChanges && file.HasUnstagedChanges {
gui.State.SplitMainPanel = true
gui.getMainView().Title = gui.Tr.SLocalize("UnstagedChanges")
gui.getSecondaryView().Title = gui.Tr.SLocalize("StagedChanges")
cmdStr := gui.GitCommand.DiffCmdStr(file, false, true)
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
if err := gui.newPtyTask("secondary", cmd); err != nil {
return err
}
} else {
gui.State.SplitMainPanel = false
if file.HasUnstagedChanges {
gui.getMainView().Title = gui.Tr.SLocalize("UnstagedChanges")
} else {
gui.getMainView().Title = gui.Tr.SLocalize("StagedChanges")
}
}
cmdStr := gui.GitCommand.DiffCmdStr(file, false, !file.HasUnstagedChanges && file.HasStagedChanges)
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
if err := gui.newPtyTask("main", cmd); err != nil {
return err
}
return nil
}
func (gui *Gui) refreshFiles() error {
gui.State.RefreshingFilesMutex.Lock()
gui.State.IsRefreshingFiles = true
defer func() {
gui.State.IsRefreshingFiles = false
gui.State.RefreshingFilesMutex.Unlock()
}()
selectedFile, _ := gui.getSelectedFile()
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 {
displayStrings := presentation.GetFileListDisplayStrings(gui.State.Files, gui.State.Diff.Ref)
gui.renderDisplayStrings(filesView, displayStrings)
if g.CurrentView() == filesView || (g.CurrentView() == gui.getMainView() && g.CurrentView().Context == "merging") {
newSelectedFile, _ := gui.getSelectedFile()
alreadySelected := newSelectedFile.Name == selectedFile.Name
return gui.selectFile(alreadySelected)
}
return nil
})
return nil
}
// 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)
@@ -134,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, len(files))
result := make([]commands.File, 0)
for _, file := range files {
if file.Tracked {
result = append(result, file)
@@ -146,40 +38,15 @@ func (gui *Gui) trackedFiles() []*commands.File {
}
func (gui *Gui) stageSelectedFile(g *gocui.Gui) error {
file, err := gui.getSelectedFile()
file, err := gui.getSelectedFile(g)
if err != nil {
return err
}
return gui.GitCommand.StageFile(file.Name)
}
func (gui *Gui) handleEnterFile(g *gocui.Gui, v *gocui.View) error {
return gui.enterFile(false, -1)
}
func (gui *Gui) enterFile(forceSecondaryFocused bool, selectedLineIdx int) error {
file, err := gui.getSelectedFile()
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
}
return nil
}
if file.HasInlineMergeConflicts {
return gui.handleSwitchToMerge(gui.g, gui.getFilesView())
}
if file.HasMergeConflicts {
return gui.createErrorPanel(gui.Tr.SLocalize("FileStagingRequirements"))
}
gui.changeMainViewsContext("staging")
if err := gui.switchFocus(gui.g, gui.getFilesView(), gui.getMainView()); err != nil {
return err
}
return gui.refreshStagingPanel(forceSecondaryFocused, selectedLineIdx)
}
func (gui *Gui) handleFilePress(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile()
file, err := gui.getSelectedFile(g)
if err != nil {
if err == gui.Errors.ErrNoFiles {
return nil
@@ -187,397 +54,358 @@ func (gui *Gui) handleFilePress(g *gocui.Gui, v *gocui.View) error {
return err
}
if file.HasInlineMergeConflicts {
if file.HasMergeConflicts {
return gui.handleSwitchToMerge(g, v)
}
if file.HasUnstagedChanges {
err = gui.GitCommand.StageFile(file.Name)
gui.GitCommand.StageFile(file.Name)
} else {
err = gui.GitCommand.UnStageFile(file.Name, file.Tracked)
}
if err != nil {
return gui.surfaceError(err)
gui.GitCommand.UnStageFile(file.Name, file.Tracked)
}
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
if err := gui.refreshFiles(g); err != nil {
return err
}
return gui.selectFile(true)
return gui.handleFileSelect(g, v)
}
func (gui *Gui) allFilesStaged() bool {
for _, file := range gui.State.Files {
if file.HasUnstagedChanges {
return false
func (gui *Gui) handleAddPatch(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
if err != nil {
if err == gui.Errors.ErrNoFiles {
return nil
}
}
return true
}
func (gui *Gui) focusAndSelectFile(g *gocui.Gui, v *gocui.View) error {
if _, err := gui.g.SetCurrentView("files"); err != nil {
return err
}
return gui.selectFile(false)
}
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 !file.HasUnstagedChanges {
return gui.createErrorPanel(g, gui.Tr.SLocalize("FileHasNoUnstagedChanges"))
}
if !file.Tracked {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CannotGitAdd"))
}
sub, err := gui.GitCommand.AddPatch(file.Name)
if err != nil {
_ = gui.surfaceError(err)
}
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
return err
}
gui.SubProcess = sub
return gui.Errors.ErrSubProcess
}
return gui.selectFile(false)
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()
file, err := gui.getSelectedFile(g)
if err != nil {
return gui.surfaceError(err)
return gui.createErrorPanel(g, err.Error())
}
if file.Tracked {
return gui.createConfirmationPanel(gui.g, gui.g.CurrentView(), true, gui.Tr.SLocalize("IgnoreTracked"), gui.Tr.SLocalize("IgnoreTrackedPrompt"),
// On confirmation
func(_ *gocui.Gui, _ *gocui.View) error {
if err := gui.GitCommand.Ignore(file.Name); err != nil {
return err
}
if err := gui.GitCommand.RemoveTrackedFiles(file.Name); err != nil {
return err
}
return gui.refreshSidePanels(refreshOptions{scope: []int{FILES}})
}, nil)
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantIgnoreTrackFiles"))
}
if err := gui.GitCommand.Ignore(file.Name); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{scope: []int{FILES}})
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.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)
}
gui.renderString(g, "commitMessage", skipHookPreifx)
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.GitCommand.WorkingTreeState() == "normal" {
return gui.createErrorPanel(gui.Tr.SLocalize("NoStagedFilesToCommit"))
if len(gui.stagedFiles()) == 0 && !gui.State.HasMergeConflicts {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit"))
}
commitMessageView := gui.getCommitMessageView()
prefixPattern := gui.Config.GetUserConfig().GetString("git.commitPrefixes." + utils.GetCurrentRepoName() + ".pattern")
prefixReplace := gui.Config.GetUserConfig().GetString("git.commitPrefixes." + utils.GetCurrentRepoName() + ".replace")
if len(prefixPattern) > 0 && len(prefixReplace) > 0 {
rgx, err := regexp.Compile(prefixPattern)
if err != nil {
return gui.createErrorPanel(fmt.Sprintf("%s: %s", gui.Tr.SLocalize("commitPrefixPatternError"), err.Error()))
}
prefix := rgx.ReplaceAllString(gui.getCheckedOutBranch().Name, prefixReplace)
gui.renderString(g, "commitMessage", prefix)
if err := commitMessageView.SetCursor(len(prefix), 0); err != nil {
return err
}
}
commitMessageView := gui.getCommitMessageView(g)
g.Update(func(g *gocui.Gui) error {
if _, err := g.SetViewOnTop("commitMessage"); err != nil {
return err
}
if err := gui.switchFocus(g, filesView, commitMessageView); err != nil {
return err
}
gui.RenderCommitLength()
g.SetViewOnTop("commitMessage")
gui.switchFocus(g, filesView, commitMessageView)
return nil
})
return nil
}
func (gui *Gui) handleAmendCommitPress(g *gocui.Gui, filesView *gocui.View) error {
if len(gui.stagedFiles()) == 0 && gui.GitCommand.WorkingTreeState() == "normal" {
return gui.createErrorPanel(gui.Tr.SLocalize("NoStagedFilesToCommit"))
}
if len(gui.State.Commits) == 0 {
return gui.createErrorPanel(gui.Tr.SLocalize("NoCommitToAmend"))
}
title := strings.Title(gui.Tr.SLocalize("AmendLastCommit"))
question := gui.Tr.SLocalize("SureToAmend")
return gui.createConfirmationPanel(g, filesView, true, title, question, func(g *gocui.Gui, v *gocui.View) error {
ok, err := gui.runSyncOrAsyncCommand(gui.GitCommand.AmendHead())
if err != nil {
return err
}
if !ok {
return nil
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}, nil)
}
// handleCommitEditorPress - handle when the user wants to commit changes via
// their editor rather than via the popup panel
func (gui *Gui) handleCommitEditorPress(g *gocui.Gui, filesView *gocui.View) error {
if len(gui.stagedFiles()) == 0 && gui.GitCommand.WorkingTreeState() == "normal" {
return gui.createErrorPanel(gui.Tr.SLocalize("NoStagedFilesToCommit"))
if len(gui.stagedFiles()) == 0 && !gui.State.HasMergeConflicts {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit"))
}
gui.PrepareSubProcess(g, "git", "commit")
return nil
}
// 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
})
}
func (gui *Gui) editFile(filename string) error {
_, err := gui.runSyncOrAsyncCommand(gui.OSCommand.EditFile(filename))
return err
}
func (gui *Gui) handleFileEdit(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile()
if err != nil {
return gui.surfaceError(err)
}
return gui.editFile(file.Name)
}
func (gui *Gui) handleFileOpen(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile()
if err != nil {
return gui.surfaceError(err)
}
return gui.openFile(file.Name)
}
func (gui *Gui) handleRefreshFiles(g *gocui.Gui, v *gocui.View) error {
return gui.refreshSidePanels(refreshOptions{scope: []int{FILES}})
}
func (gui *Gui) refreshStateFiles() error {
// get files to stage
files := gui.GitCommand.GetStatusFiles()
gui.State.Files = gui.GitCommand.MergeStatusFiles(gui.State.Files, files)
if err := gui.fileWatcher.addFilesToFileWatcher(files); err != nil {
return err
}
gui.refreshSelectedLine(&gui.State.Panels.Files.SelectedLine, len(gui.State.Files))
return nil
}
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 err
}
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 err
}
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(g)
}
func (gui *Gui) refreshStateFiles() {
// get files to stage
files := gui.GitCommand.GetStatusFiles()
gui.State.Files = gui.GitCommand.MergeStatusFiles(gui.State.Files, files)
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) {
item, err := gui.getSelectedFile()
item, err := gui.getSelectedFile(g)
if err != nil {
if err != gui.Errors.ErrNoFiles {
return "", err
}
return "", gui.newStringTask("main", gui.Tr.SLocalize("NoFilesDisplay"))
}
if item.Type != "file" {
return "", gui.newStringTask("main", gui.Tr.SLocalize("NotAFile"))
return "", gui.renderString(g, "main", gui.Tr.SLocalize("NoFilesDisplay"))
}
cat, err := gui.GitCommand.CatFile(item.Name)
if err != nil {
gui.Log.Error(err)
return "", gui.newStringTask("main", err.Error())
panic(err)
}
return cat, nil
}
func (gui *Gui) handlePullFiles(g *gocui.Gui, v *gocui.View) error {
// if we have no upstream branch we need to set that first
currentBranch := gui.currentBranch()
if currentBranch.Pullables == "?" {
// see if we have this branch in our config with an upstream
conf, err := gui.GitCommand.Repo.Config()
if err != nil {
return gui.surfaceError(err)
}
for branchName, branch := range conf.Branches {
if branchName == currentBranch.Name {
return gui.pullFiles(v, fmt.Sprintf("%s %s", branch.Remote, branchName))
}
}
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("EnterUpstream"), "origin/"+currentBranch.Name, func(g *gocui.Gui, v *gocui.View) error {
upstream := gui.trimmedContent(v)
if err := gui.GitCommand.SetUpstreamBranch(upstream); err != nil {
errorMessage := err.Error()
if strings.Contains(errorMessage, "does not exist") {
errorMessage = fmt.Sprintf("upstream branch %s not found.\nIf you expect it to exist, you should fetch (with 'f').\nOtherwise, you should push (with 'shift+P')", upstream)
}
return gui.createErrorPanel(errorMessage)
}
return gui.pullFiles(v, "")
})
}
return gui.pullFiles(v, "")
}
func (gui *Gui) pullFiles(v *gocui.View, args string) 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(args, func(passOrUname string) string {
unamePassOpend = true
return gui.waitForPassUname(gui.g, v, passOrUname)
})
gui.HandleCredentialsPopup(gui.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, upstream string, args string) 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.getCheckedOutBranch().Name
err := gui.GitCommand.Push(branchName, force, upstream, args, 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
currentBranch := gui.currentBranch()
if currentBranch.Pullables == "?" {
// see if we have this branch in our config with an upstream
conf, err := gui.GitCommand.Repo.Config()
if err != nil {
return gui.surfaceError(err)
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)
}
for branchName, branch := range conf.Branches {
if branchName == currentBranch.Name {
return gui.pushWithForceFlag(g, v, false, "", fmt.Sprintf("%s %s", branch.Remote, branchName))
}
}
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("EnterUpstream"), "origin "+currentBranch.Name, func(g *gocui.Gui, v *gocui.View) error {
return gui.pushWithForceFlag(g, v, false, gui.trimmedContent(v), "")
})
} else if currentBranch.Pullables == "0" {
return gui.pushWithForceFlag(g, v, false, "", "")
}
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("ForcePush"), gui.Tr.SLocalize("ForcePushPrompt"), func(g *gocui.Gui, v *gocui.View) error {
return gui.pushWithForceFlag(g, v, true, "", "")
}, nil)
}
func (gui *Gui) handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile()
if err != nil {
if err != gui.Errors.ErrNoFiles {
return gui.surfaceError(err)
}
return nil
}
if !file.HasInlineMergeConflicts {
return gui.createErrorPanel(gui.Tr.SLocalize("FileNoMergeCons"))
}
gui.changeMainViewsContext("merging")
if err := gui.switchFocus(g, v, gui.getMainView()); err != nil {
return err
}
return gui.refreshMergePanel()
}
func (gui *Gui) openFile(filename string) error {
if err := gui.OSCommand.OpenFile(filename); err != nil {
return gui.surfaceError(err)
}
}()
return nil
}
func (gui *Gui) anyFilesWithMergeConflicts() bool {
for _, file := range gui.State.Files {
if file.HasMergeConflicts {
return true
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 err
}
return nil
}
return false
}
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
})
}
func (gui *Gui) handleCreateStashMenu(g *gocui.Gui, v *gocui.View) error {
menuItems := []*menuItem{
{
displayString: gui.Tr.SLocalize("stashAllChanges"),
onPress: func() error {
return gui.handleStashSave(gui.GitCommand.StashSave)
},
},
{
displayString: gui.Tr.SLocalize("stashStagedChanges"),
onPress: func() error {
return gui.handleStashSave(gui.GitCommand.StashSaveStagedChanges)
},
},
if !file.HasMergeConflicts {
return gui.createErrorPanel(g, gui.Tr.SLocalize("FileNoMergeCons"))
}
return gui.createMenu(gui.Tr.SLocalize("stashOptions"), menuItems, createMenuOptions{showCancel: true})
gui.switchFocus(g, v, mergeView)
return gui.refreshMergePanel(g)
}
func (gui *Gui) handleStashChanges(g *gocui.Gui, v *gocui.View) error {
return gui.handleStashSave(gui.GitCommand.StashSave)
func (gui *Gui) handleAbortMerge(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.AbortMerge(); err != nil {
return gui.createErrorPanel(g, err.Error())
}
gui.createMessagePanel(g, v, "", gui.Tr.SLocalize("MergeAborted"))
gui.refreshStatus(g)
return gui.refreshFiles(g)
}
func (gui *Gui) handleCreateResetToUpstreamMenu(g *gocui.Gui, v *gocui.View) error {
return gui.createResetMenu("@{upstream}")
}
func (gui *Gui) onFilesPanelSearchSelect(selectedLine int) error {
gui.State.Panels.Files.SelectedLine = selectedLine
return gui.focusAndSelectFile(gui.g, gui.getFilesView())
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 gui.refreshFiles(g)
}, nil)
}
-21
View File
@@ -1,21 +0,0 @@
package gui
import "github.com/jesseduffield/gocui"
func (gui *Gui) inFilterMode() bool {
return gui.State.FilterPath != ""
}
func (gui *Gui) validateNotInFilterMode() (bool, error) {
if gui.inFilterMode() {
return false, gui.createConfirmationPanel(gui.g, gui.g.CurrentView(), true, gui.Tr.SLocalize("MustExitFilterModeTitle"), gui.Tr.SLocalize("MustExitFilterModePrompt"), func(*gocui.Gui, *gocui.View) error {
return gui.exitFilterMode()
}, nil)
}
return true, nil
}
func (gui *Gui) exitFilterMode() error {
gui.State.FilterPath = ""
return gui.Errors.ErrRestart
}
-62
View File
@@ -1,62 +0,0 @@
package gui
import (
"fmt"
"strings"
"github.com/jesseduffield/gocui"
)
func (gui *Gui) handleCreateFilteringMenuPanel(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
fileName := ""
switch v.Name() {
case "files":
file, err := gui.getSelectedFile()
if err == nil {
fileName = file.Name
}
case "commitFiles":
file := gui.getSelectedCommitFile()
if file != nil {
fileName = file.Name
}
}
menuItems := []*menuItem{}
if fileName != "" {
menuItems = append(menuItems, &menuItem{
displayString: fmt.Sprintf("%s '%s'", gui.Tr.SLocalize("filterBy"), fileName),
onPress: func() error {
gui.State.FilterPath = fileName
return gui.Errors.ErrRestart
},
})
}
menuItems = append(menuItems, &menuItem{
displayString: gui.Tr.SLocalize("filterPathOption"),
onPress: func() error {
return gui.createPromptPanel(gui.g, v, gui.Tr.SLocalize("enterFileName"), "", func(g *gocui.Gui, promptView *gocui.View) error {
gui.State.FilterPath = strings.TrimSpace(promptView.Buffer())
return gui.Errors.ErrRestart
})
},
})
if gui.inFilterMode() {
menuItems = append(menuItems, &menuItem{
displayString: gui.Tr.SLocalize("exitFilterMode"),
onPress: func() error {
gui.State.FilterPath = ""
return gui.Errors.ErrRestart
},
})
}
return gui.createMenu(gui.Tr.SLocalize("FilteringMenuTitle"), menuItems, createMenuOptions{showCancel: true})
}
-90
View File
@@ -1,90 +0,0 @@
package gui
import (
"fmt"
"regexp"
"strings"
"github.com/jesseduffield/gocui"
)
func (gui *Gui) gitFlowFinishBranch(gitFlowConfig string, branchName string) error {
// need to find out what kind of branch this is
prefix := strings.SplitAfterN(branchName, "/", 2)[0]
suffix := strings.Replace(branchName, prefix, "", 1)
branchType := ""
for _, line := range strings.Split(strings.TrimSpace(gitFlowConfig), "\n") {
if strings.HasPrefix(line, "gitflow.prefix.") && strings.HasSuffix(line, prefix) {
// now I just need to how do you say
regex := regexp.MustCompile("gitflow.prefix.([^ ]*) .*")
matches := regex.FindAllStringSubmatch(line, 1)
if len(matches) > 0 && len(matches[0]) > 1 {
branchType = matches[0][1]
break
}
}
}
if branchType == "" {
return gui.createErrorPanel(gui.Tr.SLocalize("NotAGitFlowBranch"))
}
subProcess := gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "finish", suffix)
gui.SubProcess = subProcess
return gui.Errors.ErrSubProcess
}
func (gui *Gui) handleCreateGitFlowMenu(g *gocui.Gui, v *gocui.View) error {
branch := gui.getSelectedBranch()
if branch == nil {
return nil
}
// get config
gitFlowConfig, err := gui.OSCommand.RunCommandWithOutput("git config --local --get-regexp gitflow")
if err != nil {
return gui.createErrorPanel("You need to install git-flow and enable it in this repo to use git-flow features")
}
startHandler := func(branchType string) func() error {
return func() error {
title := gui.Tr.TemplateLocalize("NewBranchNamePrompt", map[string]interface{}{"branchType": branchType})
return gui.createPromptPanel(gui.g, gui.getMenuView(), title, "", func(g *gocui.Gui, v *gocui.View) error {
name := gui.trimmedContent(v)
subProcess := gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "start", name)
gui.SubProcess = subProcess
return gui.Errors.ErrSubProcess
})
}
}
menuItems := []*menuItem{
{
// not localising here because it's one to one with the actual git flow commands
displayString: fmt.Sprintf("finish branch '%s'", branch.Name),
onPress: func() error {
return gui.gitFlowFinishBranch(gitFlowConfig, branch.Name)
},
},
{
displayString: "start feature",
onPress: startHandler("feature"),
},
{
displayString: "start hotfix",
onPress: startHandler("hotfix"),
},
{
displayString: "start bugfix",
onPress: startHandler("bugfix"),
},
{
displayString: "start release",
onPress: startHandler("release"),
},
}
return gui.createMenu("git flow", menuItems, createMenuOptions{})
}
-180
View File
@@ -1,180 +0,0 @@
package gui
import (
"math"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) nextScreenMode(g *gocui.Gui, v *gocui.View) error {
gui.State.ScreenMode = utils.NextIntInCycle([]int{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode)
// commits render differently depending on whether we're in fullscreen more or not
if err := gui.refreshCommitsViewWithSelection(); err != nil {
return err
}
// same with branches
if err := gui.refreshBranchesViewWithSelection(); err != nil {
return err
}
return nil
}
func (gui *Gui) prevScreenMode(g *gocui.Gui, v *gocui.View) error {
gui.State.ScreenMode = utils.PrevIntInCycle([]int{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode)
// commits render differently depending on whether we're in fullscreen more or not
if err := gui.refreshCommitsViewWithSelection(); err != nil {
return err
}
// same with branches
if err := gui.refreshBranchesViewWithSelection(); err != nil {
return err
}
return nil
}
func (gui *Gui) scrollUpView(viewName string) error {
mainView, _ := gui.g.View(viewName)
ox, oy := mainView.Origin()
newOy := int(math.Max(0, float64(oy-gui.Config.GetUserConfig().GetInt("gui.scrollHeight"))))
return mainView.SetOrigin(ox, newOy)
}
func (gui *Gui) scrollDownView(viewName string) error {
mainView, _ := gui.g.View(viewName)
ox, oy := mainView.Origin()
y := oy
if !gui.Config.GetUserConfig().GetBool("gui.scrollPastBottom") {
_, sy := mainView.Size()
y += sy
}
scrollHeight := gui.Config.GetUserConfig().GetInt("gui.scrollHeight")
if y < mainView.LinesHeight() {
if err := mainView.SetOrigin(ox, oy+scrollHeight); err != nil {
return err
}
}
if manager, ok := gui.viewBufferManagerMap[viewName]; ok {
manager.ReadLines(scrollHeight)
}
return nil
}
func (gui *Gui) scrollUpMain(g *gocui.Gui, v *gocui.View) error {
return gui.scrollUpView("main")
}
func (gui *Gui) scrollDownMain(g *gocui.Gui, v *gocui.View) error {
return gui.scrollDownView("main")
}
func (gui *Gui) scrollUpSecondary(g *gocui.Gui, v *gocui.View) error {
return gui.scrollUpView("secondary")
}
func (gui *Gui) scrollDownSecondary(g *gocui.Gui, v *gocui.View) error {
return gui.scrollDownView("secondary")
}
func (gui *Gui) scrollUpConfirmationPanel(g *gocui.Gui, v *gocui.View) error {
if v.Editable {
return nil
}
return gui.scrollUpView("confirmation")
}
func (gui *Gui) scrollDownConfirmationPanel(g *gocui.Gui, v *gocui.View) error {
if v.Editable {
return nil
}
return gui.scrollDownView("confirmation")
}
func (gui *Gui) handleRefresh(g *gocui.Gui, v *gocui.View) error {
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}
func (gui *Gui) handleMouseDownMain(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
switch g.CurrentView().Name() {
case "files":
return gui.enterFile(false, v.SelectedLineIdx())
case "commitFiles":
return gui.enterCommitFile(v.SelectedLineIdx())
}
return nil
}
func (gui *Gui) handleMouseDownSecondary(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
switch g.CurrentView().Name() {
case "files":
return gui.enterFile(true, v.SelectedLineIdx())
}
return nil
}
func (gui *Gui) handleInfoClick(g *gocui.Gui, v *gocui.View) error {
if !gui.g.Mouse {
return nil
}
cx, _ := v.Cursor()
width, _ := v.Size()
// if we're in the normal context there will be a donate button here
// if we have ('reset') at the end then
if gui.inFilterMode() {
if width-cx <= len(gui.Tr.SLocalize("(reset)")) {
return gui.exitFilterMode()
} else {
return nil
}
}
if gui.inDiffMode() {
if width-cx <= len(gui.Tr.SLocalize("(reset)")) {
return gui.exitDiffMode()
} else {
return nil
}
}
if cx <= len(gui.Tr.SLocalize("Donate")) {
return gui.OSCommand.OpenLink("https://github.com/sponsors/jesseduffield")
}
return nil
}
func (gui *Gui) fetch(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, true, gui.Tr.SLocalize("Error"), coloredMessage, close, close)
}
gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, COMMITS, REMOTES, TAGS}, mode: ASYNC})
return unamePassOpend, err
}
+264 -417
View File
@@ -1,45 +1,28 @@
package gui
import (
"fmt"
"io/ioutil"
"os"
"runtime"
"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/tasks"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/updates"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/mattn/go-runewidth"
"github.com/sirupsen/logrus"
)
const (
SCREEN_NORMAL int = iota
SCREEN_HALF
SCREEN_FULL
)
const StartupPopupVersion = 1
// OverlappingEdges determines if panel edges overlap
var OverlappingEdges = false
@@ -48,8 +31,6 @@ var OverlappingEdges = false
type SentinelErrors struct {
ErrSubProcess error
ErrNoFiles error
ErrSwitchRepo error
ErrRestart error
}
// GenerateSentinelErrors makes the sentinel errors for the gui. We're defining it here
@@ -66,8 +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"),
ErrRestart: errors.New("restarting"),
}
}
@@ -76,286 +55,285 @@ 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
fileWatcher *fileWatcher
viewBufferManagerMap map[string]*tasks.ViewBufferManager
stopChan chan struct{}
}
// for now the staging panel state, unlike the other panel states, is going to be
// non-mutative, so that we don't accidentally end up
// with mismatches of data. We might change this in the future
type lineByLinePanelState struct {
SelectedLineIdx int
FirstLineIdx int
LastLineIdx int
Diff string
PatchParser *commands.PatchParser
SelectMode int // one of LINE, HUNK, or RANGE
SecondaryFocused bool // this is for if we show the left or right panel
}
type mergingPanelState struct {
ConflictIndex int
ConflictTop bool
Conflicts []commands.Conflict
EditHistory *stack.Stack
}
type filePanelState struct {
SelectedLine int
}
// TODO: consider splitting this out into the window and the branches view
type branchPanelState struct {
SelectedLine int
}
type remotePanelState struct {
SelectedLine int
}
type remoteBranchesState struct {
SelectedLine int
}
type tagsPanelState struct {
SelectedLine int
}
type commitPanelState struct {
SelectedLine int
LimitCommits bool
}
type reflogCommitPanelState struct {
SelectedLine int
}
type stashPanelState struct {
SelectedLine int
}
type menuPanelState struct {
SelectedLine int
OnPress func(g *gocui.Gui, v *gocui.View) error
}
type commitFilesPanelState struct {
SelectedLine int
}
type panelStates struct {
Files *filePanelState
Branches *branchPanelState
Remotes *remotePanelState
RemoteBranches *remoteBranchesState
Tags *tagsPanelState
Commits *commitPanelState
ReflogCommits *reflogCommitPanelState
Stash *stashPanelState
Menu *menuPanelState
LineByLine *lineByLinePanelState
Merging *mergingPanelState
CommitFiles *commitFilesPanelState
}
type searchingState struct {
view *gocui.View
isSearching bool
searchString string
}
// startup stages so we don't need to load everything at once
const (
INITIAL = iota
COMPLETE
)
// if ref is blank we're not diffing anything
type DiffState struct {
Ref string
Reverse bool
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
// FilteredReflogCommits are the ones that appear in the reflog panel.
// when in filtering mode we only include the ones that match the given path
FilteredReflogCommits []*commands.Commit
// ReflogCommits are the ones used by the branches panel to obtain recency values
// if we're not in filtering mode, CommitFiles and FilteredReflogCommits will be
// one and the same
ReflogCommits []*commands.Commit
Remotes []*commands.Remote
RemoteBranches []*commands.RemoteBranch
Tags []*commands.Tag
MenuItemCount int // can't store the actual list because it's of interface{} type
PreviousView string
Updating bool
Panels *panelStates
MainContext string // used to keep the main and secondary views' contexts in sync
CherryPickedCommits []*commands.Commit
SplitMainPanel bool
RetainOriginalDir bool
IsRefreshingFiles bool
RefreshingFilesMutex sync.Mutex
RefreshingStatusMutex sync.Mutex
Searching searchingState
ScreenMode int
SideView *gocui.View
Ptmx *os.File
PrevMainWidth int
PrevMainHeight int
OldInformation string
StartupStage int // one of INITIAL and COMPLETE. Allows us to not load everything at once
FilterPath string // the filename that gets passed to git log
Diff DiffState
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
}
func (gui *Gui) resetState() {
// we carry over the filter path and diff state
prevFilterPath := ""
prevDiff := DiffState{}
if gui.State != nil {
prevFilterPath = gui.State.FilterPath
prevDiff = gui.State.Diff
}
gui.State = &guiState{
Files: make([]*commands.File, 0),
PreviousView: "files",
Commits: make([]*commands.Commit, 0),
FilteredReflogCommits: make([]*commands.Commit, 0),
ReflogCommits: make([]*commands.Commit, 0),
CherryPickedCommits: make([]*commands.Commit, 0),
StashEntries: make([]*commands.StashEntry, 0),
Panels: &panelStates{
Files: &filePanelState{SelectedLine: -1},
Branches: &branchPanelState{SelectedLine: 0},
Remotes: &remotePanelState{SelectedLine: 0},
RemoteBranches: &remoteBranchesState{SelectedLine: -1},
Tags: &tagsPanelState{SelectedLine: -1},
Commits: &commitPanelState{SelectedLine: -1, LimitCommits: true},
ReflogCommits: &reflogCommitPanelState{SelectedLine: 0}, // TODO: might need to make -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(),
},
},
SideView: nil,
Ptmx: nil,
FilterPath: prevFilterPath,
Diff: prevDiff,
}
}
// for now the split view will always be on
// NewGui builds a new gui handler
func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *commands.OSCommand, tr *i18n.Localizer, config config.AppConfigurer, updater *updates.Updater, filterPath string) (*Gui, error) {
gui := &Gui{
Log: log,
GitCommand: gitCommand,
OSCommand: oSCommand,
Config: config,
Tr: tr,
Updater: updater,
statusManager: &statusManager{},
viewBufferManagerMap: map[string]*tasks.ViewBufferManager{},
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),
StashEntries: make([]commands.StashEntry, 0),
ConflictIndex: 0,
ConflictTop: true,
Conflicts: make([]commands.Conflict, 0),
EditHistory: stack.New(),
Platform: *oSCommand.Platform,
}
gui.resetState()
gui.State.FilterPath = filterPath
gui.watchFilesForChanges()
gui := &Gui{
Log: log,
GitCommand: gitCommand,
OSCommand: oSCommand,
State: initialState,
Config: config,
Tr: tr,
}
gui.GenerateSentinelErrors()
return gui, nil
}
func (gui *Gui) scrollUpMain(g *gocui.Gui, v *gocui.View) error {
mainView, _ := g.View("main")
ox, oy := mainView.Origin()
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()
if oy < len(mainView.BufferLines()) {
return mainView.SetOrigin(ox, oy+gui.Config.GetUserConfig().GetInt("gui.scrollHeight"))
}
return nil
}
func (gui *Gui) handleRefresh(g *gocui.Gui, v *gocui.View) error {
return gui.refreshSidePanels(g)
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
// 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()
leftSideWidth := width / 3
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
}
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")
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 != gocui.ErrUnknownView {
return err
}
v.Title = gui.Tr.SLocalize("DiffTitle")
v.Wrap = true
v.FgColor = gocui.ColorWhite
}
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 = gocui.ColorWhite
}
filesView, err := g.SetView("files", 0, statusFilesBoundary+panelSpacing, leftSideWidth, filesBranchesBoundary, gocui.TOP|gocui.BOTTOM)
if err != nil {
if err != gocui.ErrUnknownView {
return err
}
filesView.Highlight = true
filesView.Title = gui.Tr.SLocalize("FilesTitle")
v.FgColor = gocui.ColorWhite
}
if v, err := g.SetView("branches", 0, filesBranchesBoundary+panelSpacing, leftSideWidth, commitsBranchesBoundary, gocui.TOP|gocui.BOTTOM); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = gui.Tr.SLocalize("BranchesTitle")
v.FgColor = gocui.ColorWhite
}
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("CommitsTitle")
v.FgColor = gocui.ColorWhite
}
if v, err := g.SetView("stash", 0, commitsStashBoundary+panelSpacing, leftSideWidth, optionsTop, gocui.TOP|gocui.RIGHT); err != nil {
if err != gocui.ErrUnknownView {
return err
}
v.Title = gui.Tr.SLocalize("StashTitle")
v.FgColor = gocui.ColorWhite
}
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
if v.FgColor, err = gui.GetOptionsPanelTextColor(); err != nil {
return err
}
}
if gui.getCommitMessageView(g) == nil {
// doesn't matter where this view starts because it will be hidden
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 = gocui.ColorWhite
commitMessageView.Editable = true
}
}
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, "version", version); err != nil {
return err
}
// 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
}
}
gui.resizePopupPanels(g)
return nil
}
func (gui *Gui) fetch(g *gocui.Gui) error {
gui.GitCommand.Fetch()
gui.refreshStatus(g)
return nil
}
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) goEvery(g *gocui.Gui, interval time.Duration, function func(*gocui.Gui) error) {
go func() {
for range time.Tick(interval) {
function(g)
}
}()
}
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
func (gui *Gui) Run() error {
gui.resetState()
g, err := gocui.NewGui(gocui.Output256, OverlappingEdges)
g, err := gocui.NewGui(gocui.OutputNormal, OverlappingEdges)
if err != nil {
return err
}
defer g.Close()
if gui.inFilterMode() {
gui.State.ScreenMode = SCREEN_HALF
} else {
gui.State.ScreenMode = SCREEN_NORMAL
}
g.OnSearchEscape = gui.onSearchEscape
g.SearchEscapeKey = gui.getKey("universal.return")
g.NextSearchMatchKey = gui.getKey("universal.nextMatch")
g.PrevSearchMatchKey = gui.getKey("universal.prevMatch")
gui.stopChan = make(chan struct{})
g.ASCII = runtime.GOOS == "windows" && runewidth.IsEastAsian()
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
}
popupTasks := []func(chan struct{}) error{}
if gui.Config.GetUserConfig().GetString("reporting") == "undetermined" {
popupTasks = append(popupTasks, gui.promptAnonymousReporting)
}
configPopupVersion := gui.Config.GetUserConfig().GetInt("StartupPopupVersion")
// -1 means we've disabled these popups
if configPopupVersion != -1 && configPopupVersion < StartupPopupVersion {
popupTasks = append(popupTasks, gui.showShamelessSelfPromotionMessage)
}
gui.showInitialPopups(popupTasks)
gui.goEvery(g, time.Second*60, gui.fetch)
gui.goEvery(g, time.Second*10, gui.refreshFiles)
gui.goEvery(g, time.Millisecond*10, gui.updateLoader)
gui.waitForIntro.Add(1)
if gui.Config.GetUserConfig().GetBool("git.autoFetch") {
go gui.startBackgroundFetch()
}
gui.goEvery(time.Second*10, gui.stopChan, gui.refreshFiles)
g.SetManager(gocui.ManagerFunc(gui.layout), gocui.ManagerFunc(gui.getFocusLayout()))
g.SetManagerFunc(gui.layout)
if err = gui.keybindings(g); err != nil {
return err
}
gui.Log.Warn("starting main loop")
err = g.MainLoop()
return err
}
@@ -363,158 +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 {
for _, manager := range gui.viewBufferManagerMap {
manager.Close()
}
gui.viewBufferManagerMap = map[string]*tasks.ViewBufferManager{}
if !gui.fileWatcher.Disabled {
gui.fileWatcher.Watcher.Close()
}
close(gui.stopChan)
if err == gocui.ErrQuit {
if !gui.State.RetainOriginalDir {
if err := gui.recordCurrentDirectory(); err != nil {
return err
}
}
break
} else if err == gui.Errors.ErrSwitchRepo {
continue
} else if err == gui.Errors.ErrRestart {
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) loadNewRepo() error {
gui.Updater.CheckForNewUpdate(gui.onBackgroundUpdateCheckFinish, false)
if err := gui.updateRecentRepoList(); err != nil {
return err
}
gui.waitForIntro.Done()
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil {
return err
}
return nil
}
func (gui *Gui) showInitialPopups(tasks []func(chan struct{}) error) {
gui.waitForIntro.Add(len(tasks))
done := make(chan struct{})
go func() {
for _, task := range tasks {
go func() {
if err := task(done); err != nil {
_ = gui.surfaceError(err)
}
}()
<-done
gui.waitForIntro.Done()
}
}()
}
func (gui *Gui) showShamelessSelfPromotionMessage(done chan struct{}) error {
onConfirm := func(g *gocui.Gui, v *gocui.View) error {
done <- struct{}{}
return gui.Config.WriteToUserConfig("startupPopupVersion", StartupPopupVersion)
}
return gui.createConfirmationPanel(gui.g, nil, true, gui.Tr.SLocalize("ShamelessSelfPromotionTitle"), gui.Tr.SLocalize("ShamelessSelfPromotionMessage"), onConfirm, onConfirm)
}
func (gui *Gui) promptAnonymousReporting(done chan struct{}) error {
return gui.createConfirmationPanel(gui.g, nil, true, gui.Tr.SLocalize("AnonymousReportingTitle"), gui.Tr.SLocalize("AnonymousReportingPrompt"), func(g *gocui.Gui, v *gocui.View) error {
done <- struct{}{}
return gui.Config.WriteToUserConfig("reporting", "on")
}, func(g *gocui.Gui, v *gocui.View) error {
done <- struct{}{}
return gui.Config.WriteToUserConfig("reporting", "off")
})
}
func (gui *Gui) goEvery(interval time.Duration, stop chan struct{}, function func() error) {
go func() {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
_ = function()
case <-stop:
return
}
}
}()
}
func (gui *Gui) startBackgroundFetch() {
gui.waitForIntro.Wait()
isNew := gui.Config.GetIsNewRepo()
if !isNew {
time.After(60 * time.Second)
}
_, err := gui.fetch(gui.g, gui.g.CurrentView(), false)
if err != nil && strings.Contains(err.Error(), "exit status 128") && isNew {
_ = gui.createConfirmationPanel(gui.g, gui.g.CurrentView(), true, gui.Tr.SLocalize("NoAutomaticGitFetchTitle"), gui.Tr.SLocalize("NoAutomaticGitFetchBody"), nil, nil)
} else {
gui.goEvery(time.Second*60, gui.stopChan, func() error {
_, err := gui.fetch(gui.g, gui.g.CurrentView(), false)
return err
})
}
}
// setColorScheme sets the color scheme for the app based on the user config
func (gui *Gui) setColorScheme() error {
userConfig := gui.Config.GetUserConfig()
theme.UpdateTheme(userConfig)
gui.g.FgColor = theme.InactiveBorderColor
gui.g.SelFgColor = theme.ActiveBorderColor
return nil
func (gui *Gui) quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}
+78 -1436
View File
File diff suppressed because it is too large Load Diff
-520
View File
@@ -1,520 +0,0 @@
package gui
import (
"fmt"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// getFocusLayout returns a manager function for when view gain and lose focus
func (gui *Gui) getFocusLayout() func(g *gocui.Gui) error {
var previousView *gocui.View
return func(g *gocui.Gui) error {
newView := gui.g.CurrentView()
if err := gui.onFocusChange(); err != nil {
return err
}
// for now we don't consider losing focus to a popup panel as actually losing focus
if newView != previousView && !gui.isPopupPanel(newView.Name()) {
if err := gui.onFocusLost(previousView, newView); err != nil {
return err
}
if err := gui.onFocus(newView); err != nil {
return err
}
previousView = newView
}
return nil
}
}
func (gui *Gui) onFocusChange() error {
currentView := gui.g.CurrentView()
for _, view := range gui.g.Views() {
view.Highlight = view == currentView
}
return nil
}
func (gui *Gui) onFocusLost(v *gocui.View, newView *gocui.View) error {
if v == nil {
return nil
}
if v.IsSearching() && newView.Name() != "search" {
if err := gui.onSearchEscape(); err != nil {
return err
}
}
switch v.Name() {
case "main":
// if we have lost focus to a first-class panel, we need to do some cleanup
gui.changeMainViewsContext("normal")
case "commitFiles":
if gui.State.MainContext != "patch-building" {
if _, err := gui.g.SetViewOnBottom(v.Name()); err != nil {
return err
}
}
}
gui.Log.Info(v.Name() + " focus lost")
return nil
}
func (gui *Gui) onFocus(v *gocui.View) error {
if v == nil {
return nil
}
gui.Log.Info(v.Name() + " focus gained")
return nil
}
func (gui *Gui) getViewHeights() map[string]int {
currView := gui.g.CurrentView()
currentCyclebleView := gui.State.PreviousView
if currView != nil {
viewName := currView.Name()
usePreviousView := true
for _, view := range cyclableViews {
if view == viewName {
currentCyclebleView = viewName
usePreviousView = false
break
}
}
if usePreviousView {
currentCyclebleView = gui.State.PreviousView
}
}
// unfortunate result of the fact that these are separate views, have to map explicitly
if currentCyclebleView == "commitFiles" {
currentCyclebleView = "commits"
}
_, height := gui.g.Size()
if gui.State.ScreenMode == SCREEN_FULL || gui.State.ScreenMode == SCREEN_HALF {
vHeights := map[string]int{
"status": 0,
"files": 0,
"branches": 0,
"commits": 0,
"stash": 0,
"options": 0,
}
vHeights[currentCyclebleView] = height - 1
return vHeights
}
usableSpace := height - 7
extraSpace := usableSpace - (usableSpace/3)*3
if height >= 28 {
return map[string]int{
"status": 3,
"files": (usableSpace / 3) + extraSpace,
"branches": usableSpace / 3,
"commits": usableSpace / 3,
"stash": 3,
"options": 1,
}
}
defaultHeight := 3
if height < 21 {
defaultHeight = 1
}
vHeights := map[string]int{
"status": defaultHeight,
"files": defaultHeight,
"branches": defaultHeight,
"commits": defaultHeight,
"stash": defaultHeight,
"options": defaultHeight,
}
vHeights[currentCyclebleView] = height - defaultHeight*4 - 1
return vHeights
}
// layout is called for every screen re-render e.g. when the screen is resized
func (gui *Gui) layout(g *gocui.Gui) error {
g.Highlight = true
width, height := g.Size()
information := gui.Config.GetVersion()
if gui.g.Mouse {
donate := color.New(color.FgMagenta, color.Underline).Sprint(gui.Tr.SLocalize("Donate"))
information = donate + " " + information
}
if gui.inDiffMode() {
information = utils.ColoredString(fmt.Sprintf("%s %s %s", gui.Tr.SLocalize("showingGitDiff"), "git diff "+gui.diffStr(), utils.ColoredString(gui.Tr.SLocalize("(reset)"), color.Underline)), color.FgMagenta)
} else if gui.inFilterMode() {
information = utils.ColoredString(fmt.Sprintf("%s '%s' %s", gui.Tr.SLocalize("filteringBy"), gui.State.FilterPath, utils.ColoredString(gui.Tr.SLocalize("(reset)"), color.Underline)), color.FgRed, color.Bold)
} else if len(gui.State.CherryPickedCommits) > 0 {
information = utils.ColoredString(fmt.Sprintf("%d commits copied", len(gui.State.CherryPickedCommits)), color.FgCyan)
}
minimumHeight := 9
minimumWidth := 10
if height < minimumHeight || width < minimumWidth {
v, err := g.SetView("limit", 0, 0, width-1, height-1, 0)
if err != nil {
if err.Error() != "unknown view" {
return err
}
v.Title = gui.Tr.SLocalize("NotEnoughSpace")
v.Wrap = true
_, _ = g.SetViewOnTop("limit")
}
return nil
}
vHeights := gui.getViewHeights()
optionsVersionBoundary := width - max(len(utils.Decolorise(information)), 1)
appStatus := gui.statusManager.getStatusString()
appStatusOptionsBoundary := 0
if appStatus != "" {
appStatusOptionsBoundary = len(appStatus) + 2
}
_, _ = g.SetViewOnBottom("limit")
_ = g.DeleteView("limit")
sidePanelWidthRatio := gui.Config.GetUserConfig().GetFloat64("gui.sidePanelWidth")
textColor := theme.GocuiDefaultTextColor
var leftSideWidth int
switch gui.State.ScreenMode {
case SCREEN_NORMAL:
leftSideWidth = int(float64(width) * sidePanelWidthRatio)
case SCREEN_HALF:
leftSideWidth = width/2 - 2
case SCREEN_FULL:
currentView := gui.g.CurrentView()
if currentView != nil && currentView.Name() == "main" {
leftSideWidth = 0
} else {
leftSideWidth = width - 1
}
}
mainPanelLeft := leftSideWidth + 1
mainPanelRight := width - 1
secondaryPanelLeft := width - 1
secondaryPanelTop := 0
mainPanelBottom := height - 2
if gui.State.SplitMainPanel {
if gui.State.ScreenMode == SCREEN_FULL {
mainPanelLeft = 0
panelSplitX := width/2 - 4
mainPanelRight = panelSplitX
secondaryPanelLeft = panelSplitX + 1
} else if width < 220 {
mainPanelBottom = height/2 - 1
secondaryPanelTop = mainPanelBottom + 1
secondaryPanelLeft = leftSideWidth + 1
} else {
units := 5
leftSideWidth = width / units
mainPanelLeft = leftSideWidth + 1
panelSplitX := (1 + ((units - 1) / 2)) * width / units
mainPanelRight = panelSplitX
secondaryPanelLeft = panelSplitX + 1
}
}
main := "main"
secondary := "secondary"
swappingMainPanels := gui.State.Panels.LineByLine != nil && gui.State.Panels.LineByLine.SecondaryFocused
if swappingMainPanels {
main = "secondary"
secondary = "main"
}
// reading more lines into main view buffers upon resize
prevMainView, err := gui.g.View("main")
if err == nil {
_, prevMainHeight := prevMainView.Size()
heightDiff := mainPanelBottom - prevMainHeight - 1
if heightDiff > 0 {
if manager, ok := gui.viewBufferManagerMap["main"]; ok {
manager.ReadLines(heightDiff)
}
if manager, ok := gui.viewBufferManagerMap["secondary"]; ok {
manager.ReadLines(heightDiff)
}
}
}
v, err := g.SetView(main, mainPanelLeft, 0, mainPanelRight, mainPanelBottom, gocui.LEFT)
if err != nil {
if err.Error() != "unknown view" {
return err
}
v.Title = gui.Tr.SLocalize("DiffTitle")
v.Wrap = true
v.FgColor = textColor
v.IgnoreCarriageReturns = true
}
hiddenViewOffset := 9999
hiddenSecondaryPanelOffset := 0
if !gui.State.SplitMainPanel {
hiddenSecondaryPanelOffset = hiddenViewOffset
}
secondaryView, err := g.SetView(secondary, secondaryPanelLeft+hiddenSecondaryPanelOffset, hiddenSecondaryPanelOffset+secondaryPanelTop, width-1+hiddenSecondaryPanelOffset, height-2+hiddenSecondaryPanelOffset, gocui.LEFT)
if err != nil {
if err.Error() != "unknown view" {
return err
}
secondaryView.Title = gui.Tr.SLocalize("DiffTitle")
secondaryView.Wrap = true
secondaryView.FgColor = gocui.ColorWhite
secondaryView.IgnoreCarriageReturns = true
}
if v, err := g.SetView("status", 0, 0, leftSideWidth, vHeights["status"]-1, gocui.BOTTOM|gocui.RIGHT); err != nil {
if err.Error() != "unknown view" {
return err
}
v.Title = gui.Tr.SLocalize("StatusTitle")
v.FgColor = textColor
}
filesView, err := g.SetViewBeneath("files", "status", vHeights["files"])
if err != nil {
if err.Error() != "unknown view" {
return err
}
filesView.Highlight = true
filesView.Title = gui.Tr.SLocalize("FilesTitle")
filesView.SetOnSelectItem(gui.onSelectItemWrapper(gui.onFilesPanelSearchSelect))
filesView.ContainsList = true
}
branchesView, err := g.SetViewBeneath("branches", "files", vHeights["branches"])
if err != nil {
if err.Error() != "unknown view" {
return err
}
branchesView.Title = gui.Tr.SLocalize("BranchesTitle")
branchesView.Tabs = []string{"Local Branches", "Remotes", "Tags"}
branchesView.FgColor = textColor
branchesView.SetOnSelectItem(gui.onSelectItemWrapper(gui.onBranchesPanelSearchSelect))
branchesView.ContainsList = true
}
if v, err := g.SetViewBeneath("commitFiles", "branches", vHeights["commits"]); err != nil {
if err.Error() != "unknown view" {
return err
}
v.Title = gui.Tr.SLocalize("CommitFiles")
v.FgColor = textColor
v.SetOnSelectItem(gui.onSelectItemWrapper(gui.onCommitFilesPanelSearchSelect))
v.ContainsList = true
}
commitsView, err := g.SetViewBeneath("commits", "branches", vHeights["commits"])
if err != nil {
if err.Error() != "unknown view" {
return err
}
commitsView.Title = gui.Tr.SLocalize("CommitsTitle")
commitsView.Tabs = []string{"Commits", "Reflog"}
commitsView.FgColor = textColor
commitsView.SetOnSelectItem(gui.onSelectItemWrapper(gui.onCommitsPanelSearchSelect))
commitsView.ContainsList = true
}
stashView, err := g.SetViewBeneath("stash", "commits", vHeights["stash"])
if err != nil {
if err.Error() != "unknown view" {
return err
}
stashView.Title = gui.Tr.SLocalize("StashTitle")
stashView.FgColor = textColor
stashView.SetOnSelectItem(gui.onSelectItemWrapper(gui.onStashPanelSearchSelect))
stashView.ContainsList = true
}
if v, err := g.SetView("options", appStatusOptionsBoundary-1, height-2, optionsVersionBoundary-1, height, 0); err != nil {
if err.Error() != "unknown view" {
return err
}
v.Frame = false
v.FgColor = theme.OptionsColor
}
if gui.getCommitMessageView() == nil {
// doesn't matter where this view starts because it will be hidden
if commitMessageView, err := g.SetView("commitMessage", hiddenViewOffset, hiddenViewOffset, hiddenViewOffset+10, hiddenViewOffset+10, 0); err != nil {
if err.Error() != "unknown view" {
return err
}
_, _ = g.SetViewOnBottom("commitMessage")
commitMessageView.Title = gui.Tr.SLocalize("CommitMessage")
commitMessageView.FgColor = textColor
commitMessageView.Editable = true
commitMessageView.Editor = gocui.EditorFunc(gui.commitMessageEditor)
}
}
if check, _ := g.View("credentials"); check == nil {
// doesn't matter where this view starts because it will be hidden
if credentialsView, err := g.SetView("credentials", hiddenViewOffset, hiddenViewOffset, hiddenViewOffset+10, hiddenViewOffset+10, 0); err != nil {
if err.Error() != "unknown view" {
return err
}
_, err := g.SetViewOnBottom("credentials")
if err != nil {
return err
}
credentialsView.Title = gui.Tr.SLocalize("CredentialsUsername")
credentialsView.FgColor = textColor
credentialsView.Editable = true
}
}
searchViewOffset := hiddenViewOffset
if gui.State.Searching.isSearching {
searchViewOffset = 0
}
// this view takes up one character. Its only purpose is to show the slash when searching
searchPrefix := "search: "
if searchPrefixView, err := g.SetView("searchPrefix", appStatusOptionsBoundary-1+searchViewOffset, height-2+searchViewOffset, len(searchPrefix)+searchViewOffset, height+searchViewOffset, 0); err != nil {
if err.Error() != "unknown view" {
return err
}
searchPrefixView.BgColor = gocui.ColorDefault
searchPrefixView.FgColor = gocui.ColorGreen
searchPrefixView.Frame = false
gui.setViewContent(gui.g, searchPrefixView, searchPrefix)
}
if searchView, err := g.SetView("search", appStatusOptionsBoundary-1+searchViewOffset+len(searchPrefix), height-2+searchViewOffset, optionsVersionBoundary+searchViewOffset, height+searchViewOffset, 0); err != nil {
if err.Error() != "unknown view" {
return err
}
searchView.BgColor = gocui.ColorDefault
searchView.FgColor = gocui.ColorGreen
searchView.Frame = false
searchView.Editable = true
}
if appStatusView, err := g.SetView("appStatus", -1, height-2, width, height, 0); err != nil {
if err.Error() != "unknown view" {
return err
}
appStatusView.BgColor = gocui.ColorDefault
appStatusView.FgColor = gocui.ColorCyan
appStatusView.Frame = false
if _, err := g.SetViewOnBottom("appStatus"); err != nil {
return err
}
}
informationView, err := g.SetView("information", optionsVersionBoundary-1, height-2, width, height, 0)
if err != nil {
if err.Error() != "unknown view" {
return err
}
informationView.BgColor = gocui.ColorDefault
informationView.FgColor = gocui.ColorGreen
informationView.Frame = false
gui.renderString(g, "information", information)
// doing this here because it'll only happen once
if err := gui.onInitialViewsCreation(); err != nil {
return err
}
}
if gui.State.OldInformation != information {
gui.setViewContent(g, informationView, information)
gui.State.OldInformation = information
}
if gui.g.CurrentView() == nil {
initialView := gui.getFilesView()
if gui.inFilterMode() {
initialView = gui.getCommitsView()
}
if _, err := gui.g.SetCurrentView(initialView.Name()); err != nil {
return err
}
if err := gui.switchFocus(gui.g, nil, initialView); err != nil {
return err
}
}
type listViewState struct {
selectedLine int
lineCount int
view *gocui.View
context string
}
listViews := []listViewState{
{view: filesView, context: "", selectedLine: gui.State.Panels.Files.SelectedLine, lineCount: len(gui.State.Files)},
{view: branchesView, context: "local-branches", selectedLine: gui.State.Panels.Branches.SelectedLine, lineCount: len(gui.State.Branches)},
{view: branchesView, context: "remotes", selectedLine: gui.State.Panels.Remotes.SelectedLine, lineCount: len(gui.State.Remotes)},
{view: branchesView, context: "remote-branches", selectedLine: gui.State.Panels.RemoteBranches.SelectedLine, lineCount: len(gui.State.Remotes)},
{view: commitsView, context: "branch-commits", selectedLine: gui.State.Panels.Commits.SelectedLine, lineCount: len(gui.State.Commits)},
{view: commitsView, context: "reflog-commits", selectedLine: gui.State.Panels.ReflogCommits.SelectedLine, lineCount: len(gui.State.FilteredReflogCommits)},
{view: stashView, context: "", selectedLine: gui.State.Panels.Stash.SelectedLine, lineCount: len(gui.State.StashEntries)},
}
// menu view might not exist so we check to be safe
if menuView, err := gui.g.View("menu"); err == nil {
listViews = append(listViews, listViewState{view: menuView, context: "", selectedLine: gui.State.Panels.Menu.SelectedLine, lineCount: gui.State.MenuItemCount})
}
for _, listView := range listViews {
// ignore views where the context doesn't match up with the selected line we're trying to focus
if listView.context != "" && (listView.view.Context != listView.context) {
continue
}
// check if the selected line is now out of view and if so refocus it
listView.view.FocusPoint(0, listView.selectedLine)
}
mainViewWidth, mainViewHeight := gui.getMainView().Size()
if mainViewWidth != gui.State.PrevMainWidth || mainViewHeight != gui.State.PrevMainHeight {
gui.State.PrevMainWidth = mainViewWidth
gui.State.PrevMainHeight = mainViewHeight
if err := gui.onResize(); err != nil {
return err
}
}
// here is a good place log some stuff
// if you download humanlog and do tail -f development.log | humanlog
// this will let you see these branches as prettified json
// gui.Log.Info(utils.AsJson(gui.State.Branches[0:4]))
return gui.resizeCurrentPopupPanel(g)
}
func (gui *Gui) onInitialViewsCreation() error {
gui.changeMainViewsContext("normal")
gui.getBranchesView().Context = "local-branches"
gui.getCommitsView().Context = "branch-commits"
return gui.loadNewRepo()
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
-327
View File
@@ -1,327 +0,0 @@
package gui
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
)
// Currently there are two 'pseudo-panels' that make use of this 'pseudo-panel'.
// One is the staging panel where we stage files line-by-line, the other is the
// patch building panel where we add lines of an old commit's file to a patch.
// This file contains the logic around selecting lines and displaying the diffs
// staging_panel.go and patch_building_panel.go have functions specific to their
// use cases
// these represent what select mode we're in
const (
LINE = iota
RANGE
HUNK
)
// returns whether the patch is empty so caller can escape if necessary
// both diffs should be non-coloured because we'll parse them and colour them here
func (gui *Gui) refreshLineByLinePanel(diff string, secondaryDiff string, secondaryFocused bool, selectedLineIdx int) (bool, error) {
state := gui.State.Panels.LineByLine
patchParser, err := commands.NewPatchParser(gui.Log, diff)
if err != nil {
return false, nil
}
if len(patchParser.StageableLines) == 0 {
return true, nil
}
var firstLineIdx int
var lastLineIdx int
selectMode := LINE
// if we have clicked from the outside to focus the main view we'll pass in a non-negative line index so that we can instantly select that line
if selectedLineIdx >= 0 {
selectMode = RANGE
firstLineIdx, lastLineIdx = selectedLineIdx, selectedLineIdx
} else if state != nil {
if state.SelectMode == HUNK {
// this is tricky: we need to find out which hunk we just staged based on our old `state.PatchParser` (as opposed to the new `patchParser`)
// we do this by getting the first line index of the original hunk, then
// finding the next stageable line, then getting its containing hunk
// in the new diff
selectMode = HUNK
prevNewHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0)
selectedLineIdx = patchParser.GetNextStageableLineIndex(prevNewHunk.FirstLineIdx)
newHunk := patchParser.GetHunkContainingLine(selectedLineIdx, 0)
firstLineIdx, lastLineIdx = newHunk.FirstLineIdx, newHunk.LastLineIdx
} else {
selectedLineIdx = patchParser.GetNextStageableLineIndex(state.SelectedLineIdx)
firstLineIdx, lastLineIdx = selectedLineIdx, selectedLineIdx
}
} else {
selectedLineIdx = patchParser.StageableLines[0]
firstLineIdx, lastLineIdx = selectedLineIdx, selectedLineIdx
}
gui.State.Panels.LineByLine = &lineByLinePanelState{
PatchParser: patchParser,
SelectedLineIdx: selectedLineIdx,
SelectMode: selectMode,
FirstLineIdx: firstLineIdx,
LastLineIdx: lastLineIdx,
Diff: diff,
SecondaryFocused: secondaryFocused,
}
if err := gui.refreshMainView(); err != nil {
return false, err
}
if err := gui.focusSelection(selectMode == HUNK); err != nil {
return false, err
}
secondaryView := gui.getSecondaryView()
secondaryView.Highlight = true
secondaryView.Wrap = false
secondaryPatchParser, err := commands.NewPatchParser(gui.Log, secondaryDiff)
if err != nil {
return false, nil
}
gui.g.Update(func(*gocui.Gui) error {
gui.setViewContent(gui.g, gui.getSecondaryView(), secondaryPatchParser.Render(-1, -1, nil))
return nil
})
return false, nil
}
func (gui *Gui) handleSelectPrevLine(g *gocui.Gui, v *gocui.View) error {
return gui.handleCycleLine(-1)
}
func (gui *Gui) handleSelectNextLine(g *gocui.Gui, v *gocui.View) error {
return gui.handleCycleLine(+1)
}
func (gui *Gui) handleSelectPrevHunk(g *gocui.Gui, v *gocui.View) error {
state := gui.State.Panels.LineByLine
newHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, -1)
return gui.selectNewHunk(newHunk)
}
func (gui *Gui) handleSelectNextHunk(g *gocui.Gui, v *gocui.View) error {
state := gui.State.Panels.LineByLine
newHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 1)
return gui.selectNewHunk(newHunk)
}
func (gui *Gui) selectNewHunk(newHunk *commands.PatchHunk) error {
state := gui.State.Panels.LineByLine
state.SelectedLineIdx = state.PatchParser.GetNextStageableLineIndex(newHunk.FirstLineIdx)
if state.SelectMode == HUNK {
state.FirstLineIdx, state.LastLineIdx = newHunk.FirstLineIdx, newHunk.LastLineIdx
} else {
state.FirstLineIdx, state.LastLineIdx = state.SelectedLineIdx, state.SelectedLineIdx
}
if err := gui.refreshMainView(); err != nil {
return err
}
return gui.focusSelection(true)
}
func (gui *Gui) handleCycleLine(change int) error {
state := gui.State.Panels.LineByLine
if state.SelectMode == HUNK {
newHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, change)
return gui.selectNewHunk(newHunk)
}
return gui.handleSelectNewLine(state.SelectedLineIdx + change)
}
func (gui *Gui) handleSelectNewLine(newSelectedLineIdx int) error {
state := gui.State.Panels.LineByLine
if newSelectedLineIdx < 0 {
newSelectedLineIdx = 0
} else if newSelectedLineIdx > len(state.PatchParser.PatchLines)-1 {
newSelectedLineIdx = len(state.PatchParser.PatchLines) - 1
}
state.SelectedLineIdx = newSelectedLineIdx
if state.SelectMode == RANGE {
if state.SelectedLineIdx < state.FirstLineIdx {
state.FirstLineIdx = state.SelectedLineIdx
} else {
state.LastLineIdx = state.SelectedLineIdx
}
} else {
state.LastLineIdx = state.SelectedLineIdx
state.FirstLineIdx = state.SelectedLineIdx
}
if err := gui.refreshMainView(); err != nil {
return err
}
return gui.focusSelection(false)
}
func (gui *Gui) handleMouseDown(g *gocui.Gui, v *gocui.View) error {
state := gui.State.Panels.LineByLine
if gui.popupPanelFocused() {
return nil
}
newSelectedLineIdx := v.SelectedLineIdx()
state.FirstLineIdx = newSelectedLineIdx
state.LastLineIdx = newSelectedLineIdx
state.SelectMode = RANGE
return gui.handleSelectNewLine(newSelectedLineIdx)
}
func (gui *Gui) handleMouseDrag(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
return gui.handleSelectNewLine(v.SelectedLineIdx())
}
func (gui *Gui) handleMouseScrollUp(g *gocui.Gui, v *gocui.View) error {
state := gui.State.Panels.LineByLine
if gui.popupPanelFocused() {
return nil
}
state.SelectMode = LINE
return gui.handleCycleLine(-1)
}
func (gui *Gui) handleMouseScrollDown(g *gocui.Gui, v *gocui.View) error {
state := gui.State.Panels.LineByLine
if gui.popupPanelFocused() {
return nil
}
state.SelectMode = LINE
return gui.handleCycleLine(1)
}
func (gui *Gui) getSelectedCommitFileName() string {
return gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLine].Name
}
func (gui *Gui) refreshMainView() error {
state := gui.State.Panels.LineByLine
var includedLineIndices []int
// I'd prefer not to have knowledge of contexts using this file but I'm not sure
// how to get around this
if gui.State.MainContext == "patch-building" {
filename := gui.getSelectedCommitFileName()
includedLineIndices = gui.GitCommand.PatchManager.GetFileIncLineIndices(filename)
}
colorDiff := state.PatchParser.Render(state.FirstLineIdx, state.LastLineIdx, includedLineIndices)
mainView := gui.getMainView()
mainView.Highlight = true
mainView.Wrap = false
gui.g.Update(func(*gocui.Gui) error {
gui.setViewContent(gui.g, gui.getMainView(), colorDiff)
return nil
})
return nil
}
// focusSelection works out the best focus for the staging panel given the
// selected line and size of the hunk
func (gui *Gui) focusSelection(includeCurrentHunk bool) error {
stagingView := gui.getMainView()
state := gui.State.Panels.LineByLine
_, viewHeight := stagingView.Size()
bufferHeight := viewHeight - 1
_, origin := stagingView.Origin()
firstLineIdx := state.SelectedLineIdx
lastLineIdx := state.SelectedLineIdx
if includeCurrentHunk {
hunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0)
firstLineIdx = hunk.FirstLineIdx
lastLineIdx = hunk.LastLineIdx
}
margin := 0 // we may want to have a margin in place to show context but right now I'm thinking we keep this at zero
var newOrigin int
if firstLineIdx-origin < margin {
newOrigin = firstLineIdx - margin
} else if lastLineIdx-origin > bufferHeight-margin {
newOrigin = lastLineIdx - bufferHeight + margin
} else {
newOrigin = origin
}
gui.g.Update(func(*gocui.Gui) error {
if err := stagingView.SetOrigin(0, newOrigin); err != nil {
return err
}
return stagingView.SetCursor(0, state.SelectedLineIdx-newOrigin)
})
return nil
}
func (gui *Gui) handleToggleSelectRange(g *gocui.Gui, v *gocui.View) error {
state := gui.State.Panels.LineByLine
if state.SelectMode == RANGE {
state.SelectMode = LINE
} else {
state.SelectMode = RANGE
}
state.FirstLineIdx, state.LastLineIdx = state.SelectedLineIdx, state.SelectedLineIdx
return gui.refreshMainView()
}
func (gui *Gui) handleToggleSelectHunk(g *gocui.Gui, v *gocui.View) error {
state := gui.State.Panels.LineByLine
if state.SelectMode == HUNK {
state.SelectMode = LINE
state.FirstLineIdx, state.LastLineIdx = state.SelectedLineIdx, state.SelectedLineIdx
} else {
state.SelectMode = HUNK
selectedHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0)
state.FirstLineIdx, state.LastLineIdx = selectedHunk.FirstLineIdx, selectedHunk.LastLineIdx
}
if err := gui.refreshMainView(); err != nil {
return err
}
return gui.focusSelection(state.SelectMode == HUNK)
}
func (gui *Gui) handleEscapeLineByLinePanel() {
gui.changeMainViewsContext("normal")
gui.State.Panels.LineByLine = nil
}
-212
View File
@@ -1,212 +0,0 @@
package gui
import "github.com/jesseduffield/gocui"
type listView struct {
viewName string
context string
getItemsLength func() int
getSelectedLineIdxPtr func() *int
handleFocus func(g *gocui.Gui, v *gocui.View) error
handleItemSelect func(g *gocui.Gui, v *gocui.View) error
handleClickSelectedItem func(g *gocui.Gui, v *gocui.View) error
gui *Gui
rendersToMainView bool
}
func (lv *listView) handlePrevLine(g *gocui.Gui, v *gocui.View) error {
return lv.handleLineChange(-1)
}
func (lv *listView) handleNextLine(g *gocui.Gui, v *gocui.View) error {
return lv.handleLineChange(1)
}
func (lv *listView) handleLineChange(change int) error {
if !lv.gui.isPopupPanel(lv.viewName) && lv.gui.popupPanelFocused() {
return nil
}
lv.gui.changeSelectedLine(lv.getSelectedLineIdxPtr(), lv.getItemsLength(), change)
if lv.rendersToMainView {
if err := lv.gui.resetOrigin(lv.gui.getMainView()); err != nil {
return err
}
}
view, err := lv.gui.g.View(lv.viewName)
if err != nil {
return err
}
return lv.handleItemSelect(lv.gui.g, view)
}
func (lv *listView) handleNextPage(g *gocui.Gui, v *gocui.View) error {
view, err := lv.gui.g.View(lv.viewName)
if err != nil {
return nil
}
_, height := view.Size()
delta := height - 1
if delta == 0 {
delta = 1
}
return lv.handleLineChange(delta)
}
func (lv *listView) handleGotoTop(g *gocui.Gui, v *gocui.View) error {
return lv.handleLineChange(-lv.getItemsLength())
}
func (lv *listView) handleGotoBottom(g *gocui.Gui, v *gocui.View) error {
return lv.handleLineChange(lv.getItemsLength())
}
func (lv *listView) handlePrevPage(g *gocui.Gui, v *gocui.View) error {
view, err := lv.gui.g.View(lv.viewName)
if err != nil {
return nil
}
_, height := view.Size()
delta := height - 1
if delta == 0 {
delta = 1
}
return lv.handleLineChange(-delta)
}
func (lv *listView) handleClick(g *gocui.Gui, v *gocui.View) error {
if !lv.gui.isPopupPanel(lv.viewName) && lv.gui.popupPanelFocused() {
return nil
}
selectedLineIdxPtr := lv.getSelectedLineIdxPtr()
prevSelectedLineIdx := *selectedLineIdxPtr
newSelectedLineIdx := v.SelectedLineIdx()
if newSelectedLineIdx > lv.getItemsLength()-1 {
return lv.handleFocus(lv.gui.g, v)
}
*selectedLineIdxPtr = newSelectedLineIdx
if lv.rendersToMainView {
if err := lv.gui.resetOrigin(lv.gui.getMainView()); err != nil {
return err
}
}
prevViewName := lv.gui.currentViewName()
if prevSelectedLineIdx == newSelectedLineIdx && prevViewName == lv.viewName && lv.handleClickSelectedItem != nil {
return lv.handleClickSelectedItem(lv.gui.g, v)
}
return lv.handleItemSelect(lv.gui.g, v)
}
func (gui *Gui) getListViews() []*listView {
return []*listView{
{
viewName: "menu",
getItemsLength: func() int { return gui.getMenuView().LinesHeight() },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Menu.SelectedLine },
handleFocus: gui.handleMenuSelect,
handleItemSelect: gui.handleMenuSelect,
// need to add a layer of indirection here because the callback changes during runtime
handleClickSelectedItem: gui.wrappedHandler(func() error { return gui.State.Panels.Menu.OnPress(gui.g, nil) }),
gui: gui,
rendersToMainView: false,
},
{
viewName: "files",
getItemsLength: func() int { return len(gui.State.Files) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Files.SelectedLine },
handleFocus: gui.focusAndSelectFile,
handleItemSelect: gui.focusAndSelectFile,
handleClickSelectedItem: gui.handleFilePress,
gui: gui,
rendersToMainView: true,
},
{
viewName: "branches",
context: "local-branches",
getItemsLength: func() int { return len(gui.State.Branches) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Branches.SelectedLine },
handleFocus: gui.handleBranchSelect,
handleItemSelect: gui.handleBranchSelect,
gui: gui,
rendersToMainView: true,
},
{
viewName: "branches",
context: "remotes",
getItemsLength: func() int { return len(gui.State.Remotes) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Remotes.SelectedLine },
handleFocus: gui.wrappedHandler(gui.renderRemotesWithSelection),
handleItemSelect: gui.handleRemoteSelect,
handleClickSelectedItem: gui.handleRemoteEnter,
gui: gui,
rendersToMainView: true,
},
{
viewName: "branches",
context: "remote-branches",
getItemsLength: func() int { return len(gui.State.RemoteBranches) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.RemoteBranches.SelectedLine },
handleFocus: gui.handleRemoteBranchSelect,
handleItemSelect: gui.handleRemoteBranchSelect,
gui: gui,
rendersToMainView: true,
},
{
viewName: "branches",
context: "tags",
getItemsLength: func() int { return len(gui.State.Tags) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Tags.SelectedLine },
handleFocus: gui.handleTagSelect,
handleItemSelect: gui.handleTagSelect,
gui: gui,
rendersToMainView: true,
},
{
viewName: "commits",
context: "branch-commits",
getItemsLength: func() int { return len(gui.State.Commits) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Commits.SelectedLine },
handleFocus: gui.handleCommitSelect,
handleItemSelect: gui.handleCommitSelect,
handleClickSelectedItem: gui.handleSwitchToCommitFilesPanel,
gui: gui,
rendersToMainView: true,
},
{
viewName: "commits",
context: "reflog-commits",
getItemsLength: func() int { return len(gui.State.FilteredReflogCommits) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.ReflogCommits.SelectedLine },
handleFocus: gui.handleReflogCommitSelect,
handleItemSelect: gui.handleReflogCommitSelect,
gui: gui,
rendersToMainView: true,
},
{
viewName: "stash",
getItemsLength: func() int { return len(gui.State.StashEntries) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Stash.SelectedLine },
handleFocus: gui.handleStashEntrySelect,
handleItemSelect: gui.handleStashEntrySelect,
gui: gui,
rendersToMainView: true,
},
{
viewName: "commitFiles",
getItemsLength: func() int { return len(gui.State.CommitFiles) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.CommitFiles.SelectedLine },
handleFocus: gui.handleCommitFileSelect,
handleItemSelect: gui.handleCommitFileSelect,
gui: gui,
rendersToMainView: true,
},
}
}
-120
View File
@@ -1,120 +0,0 @@
package gui
import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type menuItem struct {
displayString string
displayStrings []string
onPress func() error
}
// list panel functions
func (gui *Gui) handleMenuSelect(g *gocui.Gui, v *gocui.View) error {
v.FocusPoint(0, gui.State.Panels.Menu.SelectedLine)
return nil
}
// specific functions
func (gui *Gui) renderMenuOptions() error {
optionsMap := map[string]string{
fmt.Sprintf("%s/%s", gui.getKeyDisplay("universal.return"), gui.getKeyDisplay("universal.quit")): gui.Tr.SLocalize("close"),
fmt.Sprintf("%s %s", gui.getKeyDisplay("universal.prevItem"), gui.getKeyDisplay("universal.nextItem")): gui.Tr.SLocalize("navigate"),
gui.getKeyDisplay("universal.select"): 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)
}
type createMenuOptions struct {
showCancel bool
}
func (gui *Gui) createMenu(title string, items []*menuItem, createMenuOptions createMenuOptions) error {
if createMenuOptions.showCancel {
// this is mutative but I'm okay with that for now
items = append(items, &menuItem{
displayStrings: []string{gui.Tr.SLocalize("cancel")},
onPress: func() error {
return nil
},
})
}
gui.State.MenuItemCount = len(items)
stringArrays := make([][]string, len(items))
for i, item := range items {
if item.displayStrings == nil {
stringArrays[i] = []string{item.displayString}
} else {
stringArrays[i] = item.displayStrings
}
}
list := utils.RenderDisplayStrings(stringArrays)
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, false, list)
menuView, _ := gui.g.SetView("menu", x0, y0, x1, y1, 0)
menuView.Title = title
menuView.FgColor = theme.GocuiDefaultTextColor
menuView.ContainsList = true
menuView.Clear()
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 := items[selectedLine].onPress(); err != nil {
return err
}
if _, err := gui.g.View("menu"); err == nil {
if _, err := gui.g.SetViewOnBottom("menu"); err != nil {
return err
}
}
return gui.returnFocus(gui.g, menuView)
}
gui.State.Panels.Menu.OnPress = wrappedHandlePress
for _, key := range []gocui.Key{gocui.KeySpace, gocui.KeyEnter, 'y'} {
_ = gui.g.DeleteKeybinding("menu", key, gocui.ModNone)
if err := gui.g.SetKeybinding("menu", nil, 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
}
+80 -121
View File
@@ -5,17 +5,14 @@ package gui
import (
"bufio"
"bytes"
"fmt"
"io/ioutil"
"math"
"os"
"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"
)
@@ -23,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" || trimmedLine == "<<<<<<< ours" {
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)
}
@@ -52,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
}
@@ -69,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 {
@@ -104,7 +99,7 @@ func (gui *Gui) isIndexToDelete(i int, conflict commands.Conflict, pick string)
}
func (gui *Gui) resolveConflict(g *gocui.Gui, conflict commands.Conflict, pick string) error {
gitFile, err := gui.getSelectedFile()
gitFile, err := gui.getSelectedFile(g)
if err != nil {
return err
}
@@ -130,7 +125,7 @@ func (gui *Gui) resolveConflict(g *gocui.Gui, conflict commands.Conflict, pick s
}
func (gui *Gui) pushFileSnapshot(g *gocui.Gui) error {
gitFile, err := gui.getSelectedFile()
gitFile, err := gui.getSelectedFile(g)
if err != nil {
return err
}
@@ -138,164 +133,128 @@ 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)
gitFile, err := gui.getSelectedFile()
prevContent := gui.State.EditHistory.Pop().(string)
gitFile, err := gui.getSelectedFile(g)
if err != nil {
return err
}
if err := ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644); err != nil {
return err
}
return gui.refreshMergePanel()
ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644)
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]
if err := gui.pushFileSnapshot(g); err != nil {
return err
}
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]
if err := gui.pushFileSnapshot(g); err != nil {
return err
}
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
}
mainView := gui.getMainView()
mainView.Wrap = false
if err := gui.newStringTask("main", content); err != nil {
if err := gui.scrollToConflict(g); err != nil {
return err
}
if err := gui.scrollToConflict(gui.g); err != nil {
return err
}
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{
fmt.Sprintf("%s %s", gui.getKeyDisplay("universal.prevItem"), gui.getKeyDisplay("universal.nextItem")): gui.Tr.SLocalize("selectHunk"),
fmt.Sprintf("%s %s", gui.getKeyDisplay("universal.prevBlock"), gui.getKeyDisplay("universal.nextBlock")): gui.Tr.SLocalize("navigateConflicts"),
gui.getKeyDisplay("universal.select"): gui.Tr.SLocalize("pickHunk"),
gui.getKeyDisplay("main.pickBothHunks"): gui.Tr.SLocalize("pickBothHunks"),
gui.getKeyDisplay("universal.undo"): gui.Tr.SLocalize("undo"),
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"),
"b": gui.Tr.SLocalize("pickBothHunks"),
"z": gui.Tr.SLocalize("undo"),
})
}
func (gui *Gui) handleEscapeMerge(g *gocui.Gui, v *gocui.View) error {
gui.State.Panels.Merging.EditHistory = stack.New()
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); 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.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
return err
}
// if we got conflicts after unstashing, we don't want to call any git
// commands to continue rebasing/merging here
if gui.GitCommand.WorkingTreeState() == "normal" {
return gui.handleEscapeMerge(gui.g, gui.getMainView())
}
// if there are no more files with merge conflicts, we should ask whether the user wants to continue
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(), true, "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)
}
-58
View File
@@ -1,58 +0,0 @@
package gui
import (
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) getBindings(v *gocui.View) []*Binding {
var (
bindingsGlobal, bindingsPanel []*Binding
)
bindings := gui.GetInitialKeybindings()
for _, binding := range bindings {
if GetKeyDisplay(binding.Key) != "" && binding.Description != "" {
switch binding.ViewName {
case "":
bindingsGlobal = append(bindingsGlobal, binding)
case v.Name():
if len(binding.Contexts) == 0 || utils.IncludesString(binding.Contexts, v.Context) {
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)
menuItems := make([]*menuItem, len(bindings))
for i, binding := range bindings {
innerBinding := binding // note to self, never close over loop variables
menuItems[i] = &menuItem{
displayStrings: []string{GetKeyDisplay(innerBinding.Key), innerBinding.Description},
onPress: func() error {
if innerBinding.Key == nil {
return nil
}
if err := gui.handleMenuClose(g, v); err != nil {
return err
}
return innerBinding.Handler(g, v)
},
}
}
return gui.createMenu(strings.Title(gui.Tr.SLocalize("menu")), menuItems, createMenuOptions{})
}
-105
View File
@@ -1,105 +0,0 @@
package gui
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int) error {
if !gui.GitCommand.PatchManager.CommitSelected() {
return gui.handleEscapePatchBuildingPanel(gui.g, nil)
}
gui.State.SplitMainPanel = true
gui.getMainView().Title = "Patch"
gui.getSecondaryView().Title = "Custom Patch"
// get diff from commit file that's currently selected
commitFile := gui.getSelectedCommitFile()
if commitFile == nil {
gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
return nil
}
diff, err := gui.GitCommand.ShowCommitFile(commitFile.Sha, commitFile.Name, true)
if err != nil {
return err
}
secondaryDiff := gui.GitCommand.PatchManager.RenderPatchForFile(commitFile.Name, true, false, true)
if err != nil {
return err
}
empty, err := gui.refreshLineByLinePanel(diff, secondaryDiff, false, selectedLineIdx)
if err != nil {
return err
}
if empty {
return gui.handleEscapePatchBuildingPanel(gui.g, nil)
}
return nil
}
func (gui *Gui) handleToggleSelectionForPatch(g *gocui.Gui, v *gocui.View) error {
state := gui.State.Panels.LineByLine
toggleFunc := gui.GitCommand.PatchManager.AddFileLineRange
filename := gui.getSelectedCommitFileName()
includedLineIndices := gui.GitCommand.PatchManager.GetFileIncLineIndices(filename)
currentLineIsStaged := utils.IncludesInt(includedLineIndices, state.SelectedLineIdx)
if currentLineIsStaged {
toggleFunc = gui.GitCommand.PatchManager.RemoveFileLineRange
}
// add range of lines to those set for the file
commitFile := gui.getSelectedCommitFile()
if commitFile == nil {
gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
return nil
}
toggleFunc(commitFile.Name, state.FirstLineIdx, state.LastLineIdx)
if err := gui.refreshCommitFilesView(); err != nil {
return err
}
if err := gui.refreshPatchBuildingPanel(-1); err != nil {
return err
}
return nil
}
func (gui *Gui) handleEscapePatchBuildingPanel(g *gocui.Gui, v *gocui.View) error {
gui.handleEscapeLineByLinePanel()
if gui.GitCommand.PatchManager.IsEmpty() {
gui.GitCommand.PatchManager.Reset()
gui.State.SplitMainPanel = false
}
return gui.switchFocus(gui.g, nil, gui.getCommitFilesView())
}
func (gui *Gui) refreshSecondaryPatchPanel() error {
if gui.GitCommand.PatchManager.CommitSelected() {
gui.State.SplitMainPanel = true
secondaryView := gui.getSecondaryView()
secondaryView.Highlight = true
secondaryView.Wrap = false
gui.g.Update(func(*gocui.Gui) error {
gui.setViewContent(gui.g, gui.getSecondaryView(), gui.GitCommand.PatchManager.RenderAggregatedPatchColored(false))
return nil
})
} else {
gui.State.SplitMainPanel = false
}
return nil
}
-154
View File
@@ -1,154 +0,0 @@
package gui
import (
"fmt"
"github.com/jesseduffield/gocui"
)
func (gui *Gui) handleCreatePatchOptionsMenu(g *gocui.Gui, v *gocui.View) error {
if !gui.GitCommand.PatchManager.CommitSelected() {
return gui.createErrorPanel(gui.Tr.SLocalize("NoPatchError"))
}
menuItems := []*menuItem{
{
displayString: fmt.Sprintf("remove patch from original commit (%s)", gui.GitCommand.PatchManager.CommitSha),
onPress: gui.handleDeletePatchFromCommit,
},
{
displayString: "pull patch out into index",
onPress: gui.handlePullPatchIntoWorkingTree,
},
{
displayString: "apply patch",
onPress: func() error { return gui.handleApplyPatch(false) },
},
{
displayString: "apply patch in reverse",
onPress: func() error { return gui.handleApplyPatch(true) },
},
{
displayString: "reset patch",
onPress: gui.handleResetPatch,
},
}
selectedCommit := gui.getSelectedCommit()
if selectedCommit != nil && gui.GitCommand.PatchManager.CommitSha != selectedCommit.Sha {
// adding this option to index 1
menuItems = append(
menuItems[:1],
append(
[]*menuItem{
{
displayString: fmt.Sprintf("move patch to selected commit (%s)", selectedCommit.Sha),
onPress: gui.handleMovePatchToSelectedCommit,
},
}, menuItems[1:]...,
)...,
)
}
return gui.createMenu(gui.Tr.SLocalize("PatchOptionsTitle"), menuItems, createMenuOptions{showCancel: true})
}
func (gui *Gui) getPatchCommitIndex() int {
for index, commit := range gui.State.Commits {
if commit.Sha == gui.GitCommand.PatchManager.CommitSha {
return index
}
}
return -1
}
func (gui *Gui) validateNormalWorkingTreeState() (bool, error) {
if gui.GitCommand.WorkingTreeState() != "normal" {
return false, gui.createErrorPanel(gui.Tr.SLocalize("CantPatchWhileRebasingError"))
}
if gui.GitCommand.WorkingTreeState() != "normal" {
return false, gui.createErrorPanel(gui.Tr.SLocalize("CantPatchWhileRebasingError"))
}
return true, nil
}
func (gui *Gui) returnFocusFromLineByLinePanelIfNecessary() error {
if gui.State.MainContext == "patch-building" {
return gui.handleEscapePatchBuildingPanel(gui.g, nil)
}
return nil
}
func (gui *Gui) handleDeletePatchFromCommit() error {
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
return err
}
if err := gui.returnFocusFromLineByLinePanelIfNecessary(); err != nil {
return err
}
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
commitIndex := gui.getPatchCommitIndex()
err := gui.GitCommand.DeletePatchesFromCommit(gui.State.Commits, commitIndex, gui.GitCommand.PatchManager)
return gui.handleGenericMergeCommandResult(err)
})
}
func (gui *Gui) handleMovePatchToSelectedCommit() error {
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
return err
}
if err := gui.returnFocusFromLineByLinePanelIfNecessary(); err != nil {
return err
}
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
commitIndex := gui.getPatchCommitIndex()
err := gui.GitCommand.MovePatchToSelectedCommit(gui.State.Commits, commitIndex, gui.State.Panels.Commits.SelectedLine, gui.GitCommand.PatchManager)
return gui.handleGenericMergeCommandResult(err)
})
}
func (gui *Gui) handlePullPatchIntoWorkingTree() error {
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
return err
}
if err := gui.returnFocusFromLineByLinePanelIfNecessary(); err != nil {
return err
}
pull := func(stash bool) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
commitIndex := gui.getPatchCommitIndex()
err := gui.GitCommand.PullPatchIntoIndex(gui.State.Commits, commitIndex, gui.GitCommand.PatchManager, stash)
return gui.handleGenericMergeCommandResult(err)
})
}
if len(gui.trackedFiles()) > 0 {
return gui.createConfirmationPanel(gui.g, gui.g.CurrentView(), true, gui.Tr.SLocalize("MustStashTitle"), gui.Tr.SLocalize("MustStashWarning"), func(*gocui.Gui, *gocui.View) error {
return pull(true)
}, nil)
} else {
return pull(false)
}
}
func (gui *Gui) handleApplyPatch(reverse bool) error {
if err := gui.returnFocusFromLineByLinePanelIfNecessary(); err != nil {
return err
}
if err := gui.GitCommand.PatchManager.ApplyPatches(reverse); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}
func (gui *Gui) handleResetPatch() error {
gui.GitCommand.PatchManager.Reset()
return gui.refreshCommitFilesView()
}

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