Compare commits

...

123 Commits

Author SHA1 Message Date
Jesse Duffield
3e36affa69 remove trash files 2020-03-29 21:53:25 +00:00
Jesse Duffield
97d7a8ad0c add reverse patch option 2020-03-29 21:53:25 +00:00
Jesse Duffield
b89ba365d0 unbold diff info 2020-03-29 18:31:19 +11:00
Jesse Duffield
47ff388549 some more UI logic 2020-03-29 18:26:24 +11:00
Jesse Duffield
647ab9bf0f better keybinding 2020-03-29 18:26:24 +11:00
Jesse Duffield
76431b4673 simplify things 2020-03-29 18:26:24 +11:00
Jesse Duffield
be0dd29e3a don't support files until we understand the use case 2020-03-29 18:26:24 +11:00
Jesse Duffield
40fbce91ce add new diff mode
WIP

WIP

WIP

WIP

WIP

WIP

WIP
2020-03-29 18:26:24 +11:00
Jesse Duffield
33d287d2f0 remove old diff mode code 2020-03-29 18:26:24 +11:00
Jesse Duffield
9eb1cbc514 reset main's origin when cycling views 2020-03-29 02:36:01 +00:00
Jesse Duffield
40b173118a fix conflict race condition 2020-03-29 02:36:01 +00:00
Jesse Duffield
8822c409e2 split reflog commits into ReflogCommits and FilteredReflogCommits 2020-03-29 11:37:29 +11:00
Jesse Duffield
aa750c0819 load reflog commits manually when in filter mode for branches panel 2020-03-29 11:37:29 +11:00
Jesse Duffield
d90d9d7330 reset state on each Run() call 2020-03-29 11:37:29 +11:00
Jesse Duffield
a8db672ffb refactor gui.go 2020-03-29 11:37:29 +11:00
Jesse Duffield
76b66ae26f properly reset gui state when restarting or coming back from a subprocess 2020-03-29 11:37:29 +11:00
Jesse Duffield
a2790cfe8e rename to filtered mode 2020-03-29 11:37:29 +11:00
Jesse Duffield
624ae45ebb allow scoped mode where the commits/reflog/stash panels are scoped to a file
WIP

restrict certain actions in scoped mode

WIP
2020-03-29 11:37:29 +11:00
Jesse Duffield
2756b82f57 fix width of half screen mode 2020-03-29 11:37:29 +11:00
Jesse Duffield
52f41ab0d5 update cheatsheet 2020-03-28 03:16:44 +00:00
Jesse Duffield
fbb767893e support lazyloading in commits view 2020-03-28 14:02:53 +11:00
Jesse Duffield
229f5ee48c add keybindings for paging in list panels and jumping to top/bottom 2020-03-28 14:02:53 +11:00
Jesse Duffield
96c7741ba0 add workflow for auto-merging 2020-03-28 13:22:30 +11:00
Jesse Duffield
517b7d0283 fix up some things with the patch handling stuff 2020-03-28 13:19:35 +11:00
Jesse Duffield
0c0231c3e8 autostash changes when pulling file into index 2020-03-28 13:19:35 +11:00
Jesse Duffield
a9559a5c87 move working tree state function into git.go 2020-03-28 13:19:35 +11:00
Jesse Duffield
814ee24c8d better error handling 2020-03-28 11:59:45 +11:00
Jesse Duffield
7876cddf4a remove dead code 2020-03-28 11:59:45 +11:00
Jesse Duffield
e9051355a1 fix test 2020-03-28 11:59:45 +11:00
Jesse Duffield
29316a528a better documentation 2020-03-28 11:59:45 +11:00
Jesse Duffield
036b53acf8 in fact we don't need any of these options 2020-03-28 11:59:45 +11:00
Jesse Duffield
919463ff02 actually don't even bother limiting 2020-03-28 11:59:45 +11:00
Jesse Duffield
3f7ec3f3b8 load reflog commits in two stages to speed up startup time 2020-03-28 11:59:45 +11:00
Jesse Duffield
19604214d7 discard old reflog commits when in new context 2020-03-28 11:59:45 +11:00
Jesse Duffield
f7add8d788 smarter refreshing for tags and remotes 2020-03-28 11:59:45 +11:00
Jesse Duffield
d97c230747 stop switching focus to commit files view while staging line by line 2020-03-28 11:59:45 +11:00
Jesse Duffield
906a49049e smart refreshing files 2020-03-28 11:59:45 +11:00
Jesse Duffield
c1a4bd0482 more smart refreshing
WIP

WIP

WIP

WIP

WIP

fix how diff entries are handled

WIP

WIP

WIP

WIP

WIP

WIP
2020-03-28 11:59:45 +11:00
Jesse Duffield
d0336fe16f better presentation of remotes 2020-03-28 11:59:45 +11:00
Jesse Duffield
61b4bbf74e clean up signature 2020-03-28 11:59:45 +11:00
Jesse Duffield
384c2e13d7 better refreshing for stash 2020-03-28 11:59:45 +11:00
Jesse Duffield
198d237679 more centralised handling of refreshing 2020-03-28 11:59:45 +11:00
Jesse Duffield
39315ca1e2 use wait groups when refreshing 2020-03-28 11:59:45 +11:00
Jesse Duffield
efb51eee96 more efficient refreshing 2020-03-28 11:59:45 +11:00
Jesse Duffield
fbbd16bd82 use reflogs from state to work out branch recencies 2020-03-28 11:59:45 +11:00
Jesse Duffield
bd2c1eef53 remove redundant fetch of reflog 2020-03-28 11:59:45 +11:00
Jesse Duffield
d1395b15bb use GIT_EDITOR 2020-03-27 19:26:14 +11:00
Máximo Cuadros
2d8ed5e274 *: update go-git import 2020-03-27 19:06:21 +11:00
Máximo Cuadros
6a5d8ba859 vendor: replace go-git package 2020-03-27 19:06:21 +11:00
Francisco Miamoto
320e2a6536 fix links on toc 2020-03-27 09:14:04 +11:00
Dawid Dziurla
3858118340 Merge pull request #751 from Semro/patch-1
Fix link in README
2020-03-26 16:59:04 +01:00
Semro
6420068569 Fix link in README 2020-03-26 18:46:26 +03:00
Jesse Duffield
95b147079f allow applying patch directly 2020-03-26 21:44:45 +11:00
Jesse Duffield
83757f1065 limit size of menu panel 2020-03-26 21:44:33 +11:00
Jesse Duffield
f2036b42e5 only load new reflog entries 2020-03-26 21:44:33 +11:00
Jesse Duffield
21b7d41845 relax limit on commit list and reset on branch change 2020-03-26 21:44:33 +11:00
Jesse Duffield
91a404d033 separate commits from cherry pick state 2020-03-26 21:44:33 +11:00
Jesse Duffield
d027cf969c better handling of current branch name 2020-03-26 20:37:06 +11:00
Jesse Duffield
c7f68a2ef9 delete unused assets 2020-03-26 19:18:43 +11:00
Jesse Duffield
78e55a05c1 another staging gif 2020-03-26 19:15:15 +11:00
Jesse Duffield
ca71555d0b Update README.md 2020-03-26 19:10:49 +11:00
Jesse Duffield
77fdac01ff better staging gif 2020-03-26 19:10:37 +11:00
Jesse Duffield
8301fae01e Update README.md 2020-03-26 19:04:48 +11:00
Jesse Duffield
e9161ad702 add staging gif 2020-03-26 19:04:26 +11:00
Jesse Duffield
a0a139da1f add rebasing gif 2020-03-26 18:43:10 +11:00
Francisco Miamoto
8f13d1da91 change binary releases order 2020-03-26 18:32:38 +11:00
Francisco Miamoto
d5fe9ce2c7 add table of contents to readme 2020-03-26 18:32:38 +11:00
Jesse Duffield
37acc17cf3 more lenient getting of short shas 2020-03-26 18:30:02 +11:00
Jesse Duffield
569ec5919c Update README.md 2020-03-26 09:14:14 +11:00
Dawid Dziurla
19719becf5 workflows: run goreleaser as a build step for CI 2020-03-25 21:26:15 +11:00
Dawid Dziurla
e64057b803 workflows: install gox in separate step in /tmp directory
avoid Go trying to add a dependency to go.mod file
2020-03-25 21:26:15 +11:00
Dawid Dziurla
672667aa3e goreleaser: stop ignoring arm64 build for freebsd 2020-03-25 21:26:15 +11:00
Dawid Dziurla
8a06b6067e go mod vendor 2020-03-25 21:26:15 +11:00
Dawid Dziurla
2dcc52abd0 go mod tidy 2020-03-25 21:26:15 +11:00
Dawid Dziurla
c831ad39c9 pkg: use upstream pty package 2020-03-25 21:26:15 +11:00
Jesse Duffield
0cf78ea9ad Update Undoing.md 2020-03-25 20:35:10 +11:00
Jesse Duffield
3d51fbf354 Update README.md 2020-03-25 20:32:44 +11:00
Jesse Duffield
e7a2c7cc3e update cheatsheet 2020-03-25 20:24:03 +11:00
Jesse Duffield
708a078412 document undo 2020-03-25 20:17:46 +11:00
Jesse Duffield
bbcc4b7b70 just disallow undo/redo while rebasing because you need more info than just the reflog 2020-03-25 09:39:04 +11:00
Jesse Duffield
45bba0a3c5 ignore redundant actions when undoing and redoing 2020-03-25 09:39:04 +11:00
Jesse Duffield
d105e2690a vastly improve the logic for undo and redo 2020-03-25 09:39:04 +11:00
Jesse Duffield
32d3e497c3 fix tests 2020-03-25 09:39:04 +11:00
Jesse Duffield
30a5d1b486 move into undoing file 2020-03-25 09:39:04 +11:00
Jesse Duffield
6b3ea56add refactor undo and redo 2020-03-25 09:39:04 +11:00
Jesse Duffield
c3aefdb98e stateless undos and redos 2020-03-25 09:39:04 +11:00
Jesse Duffield
094939451d more explicit env vars 2020-03-25 09:39:04 +11:00
Jesse Duffield
0e23f44b84 support reflog action prefix 2020-03-25 09:39:04 +11:00
Jesse Duffield
daecdd7c2b redoing 2020-03-25 09:39:04 +11:00
Jesse Duffield
7c8df28d01 add waiting status to checkout ref handler 2020-03-25 09:39:04 +11:00
Jesse Duffield
65917272a2 undoing status 2020-03-25 09:39:04 +11:00
Jesse Duffield
137fd80fdb note that undo functionality is experimental 2020-03-25 09:39:04 +11:00
Jesse Duffield
98fbc61221 better formatted reflog list 2020-03-25 09:39:04 +11:00
Jesse Duffield
f80d15062b use reflog undo history pointer 2020-03-25 09:39:04 +11:00
Jesse Duffield
b1b0219f04 autostash changes when hard resetting 2020-03-25 09:39:04 +11:00
Jesse Duffield
b1941c33f7 undo via rebase 2020-03-25 09:39:04 +11:00
J. B. Rainsberger
a15a7b607d Made the humor more concise and clear. 2020-03-25 09:18:25 +11:00
J. B. Rainsberger
d50283f5ee improved spelling 2020-03-25 09:18:25 +11:00
J. B. Rainsberger
6508d3b872 inject more humor into the README 2020-03-25 09:18:25 +11:00
J. B. Rainsberger
65b8cef1b8 Fixed an incorrect criticism of git in the README.
It's true that parts of git are genuinely difficult to use, so
we don't need to overstate that difficulty in order to state the
value proposition of lazygit. If `git add -p` can't split a hunk
any further, one is not _completely_ stuck, but one does need to
edit the hunk in a way that, after years, I still need a few
attempts to get right. The fact that many otherwise-experienced
git users don't even know that one can do that is itself a
testament to the UX problems that lazygit is trying to address.
2020-03-25 09:18:25 +11:00
Jesse Duffield
5d460e1e5e add tab keybindings 2020-03-23 23:25:00 +11:00
Jesse Duffield
3d3e0be7bd more compatible commands 2020-03-23 22:33:17 +11:00
Dawid Dziurla
c06c0b7133 workflows: git fetch --unshallow before goreleaser step 2020-03-22 21:49:41 +11:00
Dawid Dziurla
91f6630907 README: remove codecov badge 2020-03-22 21:32:06 +11:00
Dawid Dziurla
60085cf679 workflows: use get-tag action 2020-03-22 21:31:53 +11:00
Dawid Dziurla
389480b8fc goreleaser: ignore arm64 build for freebsd 2020-03-22 21:31:37 +11:00
Dawid Dziurla
b5c4f78e9d Remove redundant semicolon 2020-03-21 12:55:44 +11:00
Dawid Dziurla
59b0e2d70a Add GOBIN to PATH 2020-03-21 12:55:44 +11:00
Dawid Dziurla
39bd1a4628 Wording 2020-03-21 12:55:44 +11:00
Dawid Dziurla
1c1445c896 Cache builds 2020-03-21 12:55:44 +11:00
Dawid Dziurla
1e8ade2431 Use setup-go Action instead of container 2020-03-21 12:55:44 +11:00
Dawid Dziurla
a990fbc3eb Don't run codecov or golangci Actions 2020-03-21 12:55:44 +11:00
Dawid Dziurla
e5574e7fe5 Continue if lint step fails 2020-03-21 12:55:44 +11:00
Dawid Dziurla
6c8a924fad Run 4 builds in parallel 2020-03-21 12:55:44 +11:00
Dawid Dziurla
64706257ca Add golangci-lint Action 2020-03-21 12:55:44 +11:00
Dawid Dziurla
6183d92315 Fix typo in script name 2020-03-21 12:55:44 +11:00
Dawid Dziurla
31823a7405 Modify CI badge in README 2020-03-21 12:55:44 +11:00
Dawid Dziurla
85ddd623f6 Add CI workflow 2020-03-21 12:55:44 +11:00
Dawid Dziurla
9212dda9c3 Add CD workflow 2020-03-21 12:55:44 +11:00
Dawid Dziurla
93d7b37c8d Remove homebrew workflow
Will be integrated with another
2020-03-21 12:55:44 +11:00
Dawid Dziurla
8470bcd71d Remove circleci config 2020-03-21 12:55:44 +11:00
Jesse Duffield
3aab37611a show status of selected cherry picked commits 2020-03-19 21:42:21 +11:00
Jesse Duffield
8fbcc36331 allow resetting cherry picked commits selection 2020-03-19 21:42:21 +11:00
474 changed files with 19285 additions and 92520 deletions

View File

@@ -1,62 +0,0 @@
version: 2
jobs:
build:
docker:
- image: circleci/golang:1.14
environment:
GO111MODULE: "on"
working_directory: /go/src/github.com/jesseduffield/lazygit
steps:
- checkout
- run:
name: Run gofmt -s
command: |
if [ $(find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;|wc -l) -gt 0 ]; then
find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;
exit 1;
fi
- restore_cache:
keys:
- pkg-cache-{{ checksum "go.sum" }}-v5
- run:
name: Run tests
command: |
./test.sh
- run:
name: Push on codecov result
command: |
bash <(curl -s https://codecov.io/bash)
- run:
name: Compile project on every platform
command: |
go get github.com/mitchellh/gox
GOFLAGS=-mod=vendor gox -parallel 10 -os "linux freebsd netbsd windows" -osarch "darwin/i386 darwin/amd64"
- save_cache:
key: pkg-cache-{{ checksum "go.sum" }}-v5
paths:
- ~/.cache/go-build
release:
docker:
- image: circleci/golang:1.14
working_directory: /go/src/github.com/jesseduffield/lazygit
steps:
- checkout
- run:
name: Run gorelease
command: |
curl -sL https://git.io/goreleaser | bash
workflows:
version: 2
build:
jobs:
- build
release:
jobs:
- release:
filters:
tags:
only: /v[0-9]+(\.[0-9]+)*/
branches:
ignore: /.*/

28
.github/workflows/automerge.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: automerge
on:
pull_request:
types:
- labeled
- unlabeled
- synchronize
- opened
- edited
- ready_for_review
- reopened
- unlocked
pull_request_review:
types:
- submitted
check_suite:
types:
- completed
status: {}
jobs:
automerge:
runs-on: ubuntu-latest
steps:
- name: automerge
uses: "pascalgn/automerge-action@135f0bdb927d9807b5446f7ca9ecc2c51de03c4a"
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
MERGE_METHOD: rebase

32
.github/workflows/cd.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
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: Get tag
id: tag
uses: dawidd6/action-get-tag@v1
- name: Bump Homebrew
uses: dawidd6/action-homebrew-bump-formula@v1
with:
token: ${{secrets.GITHUB_API_TOKEN}}
formula: lazygit
url: "https://github.com/${{github.repository}}/archive/${{steps.tag.outputs.tag}}.tar.gz"

36
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
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

View File

@@ -1,20 +0,0 @@
name: Bump Homebrew formula
on:
push:
tags:
- 'v*'
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Get tag
id: tag
run: echo "::set-output name=tag::${GITHUB_REF##*/}"
- name: Bump Homebrew formula
uses: dawidd6/action-homebrew-bump-formula@v1.1.0
with:
token: ${{secrets.GITHUB_API_TOKEN}}
formula: lazygit
url: "https://github.com/${{github.repository}}/archive/${{steps.tag.outputs.tag}}.tar.gz"

View File

@@ -1,28 +1,50 @@
# lazygit [![CircleCI](https://circleci.com/gh/jesseduffield/lazygit.svg?style=svg)](https://circleci.com/gh/jesseduffield/lazygit) [![codecov](https://codecov.io/gh/jesseduffield/lazygit/branch/master/graph/badge.svg)](https://codecov.io/gh/jesseduffield/lazygit) [![Go Report Card](https://goreportcard.com/badge/github.com/jesseduffield/lazygit)](https://goreportcard.com/report/github.com/jesseduffield/lazygit) [![GolangCI](https://golangci.com/badges/github.com/jesseduffield/lazygit.svg)](https://golangci.com) [![GoDoc](https://godoc.org/github.com/jesseduffield/lazygit?status.svg)](http://godoc.org/github.com/jesseduffield/lazygit) [![GitHub tag](https://img.shields.io/github/tag/jesseduffield/lazygit.svg)]() [![TODOs](https://badgen.net/https/api.tickgit.com/badgen/github.com/jesseduffield/lazygit)](https://www.tickgit.com/browse?repo=github.com/jesseduffield/lazygit)
# lazygit
A simple terminal UI for git commands, written in Go with the [gocui](https://github.com/jroimartin/gocui 'gocui') library.
![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)
Rant time: You've heard it before, git is _powerful_, but what good is that power when everything is so damn hard to do? Interactive rebasing requires you to edit a goddamn TODO file in your editor? *Are you kidding me?* To stage part of a file you need to use a command line program stepping through each hunk and if a hunk can't be split down any further but contains code you don't want to stage, bad luck? *Are you KIDDING me?!* Sometimes you get asked to stash your changes when switching branches only to realise that after you switch and unstash that there weren't even any conflicts and it would have been fine to just checkout the branch directly? *YOU HAVE GOT TO BE KIDDING ME!*
A simple terminal UI for git commands, written in Go with the [gocui](https://github.com/jroimartin/gocui "gocui") library.
Rant time: You've heard it before, git is _powerful_, but what good is that power when everything is so damn hard to do? Interactive rebasing requires you to edit a goddamn TODO file in your editor? *Are you kidding me?* To stage part of a file you need to use a command line program to step through each hunk and if a hunk can't be split down any further but contains code you don't want to stage, you have to edit an arcane patch file _by hand_? *Are you KIDDING me?!* Sometimes you get asked to stash your changes when switching branches only to realise that after you switch and unstash that there weren't even any conflicts and it would have been fine to just checkout the branch directly? *YOU HAVE GOT TO BE KIDDING ME!*
If you're a mere mortal like me and you're tired of hearing how powerful git is when in your daily life it's a powerful pain in your ass, lazygit might be for you.
![Gif](/docs/resources/lazygit-example.gif)
![Gif](/docs/resources/staging.gif)
- [Installation](https://github.com/jesseduffield/lazygit#installation)
- [Usage](https://github.com/jesseduffield/lazygit#usage),
[Keybindings](/docs/keybindings)
- [Cool Features](https://github.com/jesseduffield/lazygit#cool-features)
- [Contributing](https://github.com/jesseduffield/lazygit#contributing)
- [Video Tutorial](https://youtu.be/VDXvbHZYeKY)
- [Rebase Magic Video Tutorial](https://youtu.be/4XaToVut_hs)
- [Twitch Stream](https://www.twitch.tv/jesseduffield)
## Table of contents
[<img src="https://i.imgur.com/sVEktDn.png">](https://youtu.be/CPLdltN7wgE)
- [Installation](#installation)
- [Binary releases](#binary-releases)
- [Homebrew](#homebrew)
- [MacPorts](#macports)
- [Ubuntu](#ubuntu)
- [Void Linux](#void-linux)
- [Scoop (Windows)](#scoop-windows)
- [Arch Linux](#arch-linux)
- [Fedora and CentOS 7](#fedora-and-centos-7)
- [Conda](#conda)
- [Go](#go)
- [Usage](#usage)
- [Keybindings](#keybindings)
- [Changing directory on exit](#changing-directory-on-exit)
- [Undo/Redo](#undoredo)
- [Configuration](#configuration)
- [Custom pagers](#configuration)
- [Tutorials](#tutorials)
- [Cool Features](#cool-features)
- [Contributing](#contributing)
- [Donate](#donate)
- [Alternatives](#alternatives)
Github Sponsors is matching all donations dollar-for-dollar for 12 months so if you're feeling generous consider [sponsoring me](https://github.com/sponsors/jesseduffield)
[<img src="https://i.imgur.com/sVEktDn.png">](https://youtu.be/CPLdltN7wgE)
## Installation
### Binary Releases
For Windows, Mac OS or Linux, you can download a binary release [here](../../releases).
### Homebrew
Normally the lazygit formula can be found in the Homebrew core but we suggest you tap our formula to get the frequently updated one. It works with Linux, too.
@@ -67,6 +89,7 @@ They follow upstream latest releases
```sh
sudo xbps-install -S lazygit
```
### Scoop (Windows)
You can install `lazygit` using [scoop](https://scoop.sh/). It's in the `extras` bucket:
@@ -109,10 +132,6 @@ Released versions are available for different platforms, see <https://anaconda.o
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
@@ -125,18 +144,24 @@ may need to add `~/go/bin` to your \$PATH (MacOS/Linux), or `%HOME%\go\bin`
(Windows). Not to be mistaked for `C:\Go\bin` (which is for Go's own binaries,
not apps like Lazygit).
## Usage
Call `lazygit` in your terminal inside a git repository. If you want, you can
Call `lazygit` in your terminal inside a git repository.
```sh
$ lazygit
```
If you want, you can
also add an alias for this with `echo "alias lg='lazygit'" >> ~/.zshrc` (or
whichever rc file you're using).
- Basic video tutorial [here](https://youtu.be/VDXvbHZYeKY).
- Rebase Magic tutorial [here](https://youtu.be/4XaToVut_hs)
- List of keybindings
[here](/docs/keybindings).
### Keybindings
## Changing Directory On Exit
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):
@@ -156,14 +181,25 @@ lg()
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 the [configuration docs](docs/Config.md).
Check out the [configuration docs](docs/Config.md).
## Custom Pagers
### Custom Pagers
See the [docs](docs/Custom_Pagers.md)
## Tutorials
- [Video Tutorial](https://youtu.be/VDXvbHZYeKY)
- [Rebase Magic Video Tutorial](https://youtu.be/4XaToVut_hs)
- [Twitch Stream](https://www.twitch.tv/jesseduffield)
## Cool features
- Adding files easily
@@ -179,14 +215,14 @@ See the [docs](docs/Custom_Pagers.md)
### Interactive Rebasing
![Interactive Rebasing](/docs/resources/interactive-rebase.png)
![Interactive Rebasing](/docs/resources/rebase.gif)
## Contributing
We love your input! Please check out the [contributing guide](CONTRIBUTING.md).
For contributor discussion about things not better discussed here in the repo, join the slack channel
[![Slack](/docs/resources/slack_rgb.png)](https://join.slack.com/t/lazygit/shared_invite/enQtNDE3MjIwNTYyMDA0LTM3Yjk3NzdiYzhhNTA1YjM4Y2M4MWNmNDBkOTI0YTE4YjQ1ZmI2YWRhZTgwNjg2YzhhYjg3NDBlMmQyMTI5N2M)
[![Slack](/docs/resources/slack_rgb.png)](https://join.slack.com/t/lazygit/shared_invite/zt-5bo2clzo-hB8ZTVN5dWUCqj5QFiQVLA)
## Donate

View File

@@ -53,6 +53,10 @@ Default path for the config file:
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
@@ -83,6 +87,10 @@ Default path for the config file:
prevTab: '['
nextScreenMode: '+'
prevScreenMode: '_'
undo: 'z'
redo: '<c-z>'
filteringMenu: <c-s>
diffingMenu: '<c-e>'
status:
checkForUpdate: 'u'
recentRepos: '<enter>'
@@ -126,8 +134,8 @@ Default path for the config file:
cherryPickCopyRange: 'C'
pasteCommits: 'v'
tagCommit: 'T'
toggleDiffCommit: 'i'
checkoutCommit: '<space>'
resetCherryPick: '<c-R>'
stash:
popStash: 'g'
commitFiles:
@@ -137,7 +145,6 @@ Default path for the config file:
toggleDragSelect-alt: 'V'
toggleSelectHunk: 'a'
pickBothHunks: 'b'
undo: 'z'
```
## Platform Defaults

24
docs/Undoing.md Normal file
View File

@@ -0,0 +1,24 @@
# 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.

View File

@@ -11,11 +11,20 @@
<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>
@@ -31,7 +40,11 @@
<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))
@@ -44,7 +57,11 @@
<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)
@@ -54,7 +71,11 @@
<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)
@@ -65,7 +86,11 @@
<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
@@ -77,13 +102,18 @@
<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>: start search
<kbd>]</kbd>: next tab
<kbd>[</kbd>: previous tab
</pre>
## Commits Panel (Commits Tab)
@@ -110,6 +140,12 @@
<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)
@@ -117,6 +153,11 @@
<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
@@ -139,7 +180,11 @@
<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)
@@ -202,7 +247,11 @@
<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
@@ -211,7 +260,11 @@
<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

View File

@@ -11,11 +11,20 @@
<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>
@@ -31,7 +40,11 @@
<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))
@@ -44,7 +57,11 @@
<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)
@@ -54,7 +71,11 @@
<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)
@@ -65,7 +86,11 @@
<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
@@ -77,13 +102,18 @@
<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>: start search
<kbd>]</kbd>: next tab
<kbd>[</kbd>: previous tab
</pre>
## Commits Panel (Commits Tab)
@@ -110,6 +140,12 @@
<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)
@@ -117,6 +153,11 @@
<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
@@ -139,7 +180,11 @@
<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)
@@ -202,7 +247,11 @@
<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
@@ -211,7 +260,11 @@
<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

View File

@@ -11,11 +11,20 @@
<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>
@@ -31,7 +40,11 @@
<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))
@@ -44,7 +57,11 @@
<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)
@@ -54,7 +71,11 @@
<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)
@@ -65,7 +86,11 @@
<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
@@ -77,13 +102,18 @@
<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>: start search
<kbd>]</kbd>: next tab
<kbd>[</kbd>: previous tab
</pre>
## Commity Panel (Commits Tab)
@@ -110,6 +140,12 @@
<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)
@@ -117,6 +153,11 @@
<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
@@ -139,7 +180,11 @@
<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)
@@ -152,7 +197,7 @@
<kbd>►</kbd>: select next conflict
<kbd>▲</kbd>: select top hunk
<kbd>▼</kbd>: select bottom hunk
<kbd>z</kbd>: undo
<kbd>z</kbd>: cofnij
</pre>
## Main Panel (Normal)
@@ -202,7 +247,11 @@
<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
@@ -211,7 +260,11 @@
<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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

BIN
docs/resources/rebase.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
docs/resources/staging.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 KiB

BIN
docs/resources/undo2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 KiB

9
go.mod
View File

@@ -4,15 +4,16 @@ 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/pty v1.2.1
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
@@ -20,7 +21,6 @@ require (
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/mitchellh/gox v1.0.1 // indirect
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
@@ -34,11 +34,6 @@ require (
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
github.com/stretchr/testify v1.4.0
github.com/tcnksm/go-gitconfig v0.1.2
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 // indirect
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 // indirect
golang.org/x/text v0.3.2
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/src-d/go-git.v4 v4.13.1
gopkg.in/yaml.v2 v2.2.7
)

61
go.sum
View File

@@ -23,7 +23,9 @@ github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.10-0.20191209115840-8ab47f72e854 h1:NB4neYMzyBsw52kUdkTrQm4Q05ErObCdwLvJptpfJSc=
github.com/creack/pty v1.1.10-0.20191209115840-8ab47f72e854/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -42,6 +44,14 @@ 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=
@@ -68,8 +78,6 @@ github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoA
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/go-version v1.0.0 h1:21MVWPKDphxa7ineQQTrCU5brh7OuVVAzGOCnnCPtE8=
github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
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=
@@ -78,12 +86,8 @@ 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.20200301081700-d6e485450113 h1:jHZRVJUWsU8HaQ0crocz0i0BkpOqFLDJEO/AtBp+Ecs=
github.com/jesseduffield/gocui v0.3.1-0.20200301081700-d6e485450113/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
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/pty v1.2.1 h1:7xYBiwNH0PpWqC8JmvrPq1a/ksNqyCavzWu9pbBGYWI=
github.com/jesseduffield/pty v1.2.1/go.mod h1:7jlS40+UhOqkZJDIG1B/H21xnuET/+fvbbnHCa8wSIo=
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=
@@ -104,9 +108,10 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
@@ -121,22 +126,19 @@ 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/gox v1.0.1 h1:x0jD3dcHk9a9xPSDN6YEL4xL6Qz0dvNYm8yZqui5chI=
github.com/mitchellh/gox v1.0.1/go.mod h1:ED6BioOGXMswlXa2zxfh/xdd5QhwYliBFn9V18Ap4z4=
github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
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-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
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=
@@ -155,8 +157,8 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 h1:Xuk8ma/ibJ1fOy4Ee11vHhUFHQNpHhrBneOCNHVXS5w=
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0/go.mod h1:7AwjWCpdPhkSmNAgUv5C7EJ4AbmjEB3r047r3DXWu3Y=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
@@ -183,13 +185,9 @@ github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk=
github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4=
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
@@ -210,9 +208,8 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -223,10 +220,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -243,10 +238,9 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 h1:gSbV7h1NRL2G1xTg/owz62CST1oJBmxy4QpMMregXVQ=
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@@ -257,7 +251,6 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@@ -267,17 +260,13 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg=
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE=
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=

View File

@@ -25,6 +25,9 @@ func main() {
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
@@ -61,7 +64,7 @@ func main() {
log.Fatal(err.Error())
}
app, err := app.NewApp(appConfig)
app, err := app.NewApp(appConfig, filterPath)
if err == nil {
err = app.Run()

View File

@@ -91,7 +91,7 @@ func newLogger(config config.AppConfigurer) *logrus.Entry {
}
// NewApp bootstrap a new application
func NewApp(config config.AppConfigurer) (*App, error) {
func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
app := &App{
closers: []io.Closer{},
Config: config,
@@ -121,7 +121,7 @@ func NewApp(config config.AppConfigurer) (*App, error) {
if err != nil {
return app, err
}
app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config, app.Updater)
app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config, app.Updater, filterPath)
if err != nil {
return app, err
}

View File

@@ -3,7 +3,9 @@ package commands
// Branch : A git branch
// duplicating this for now
type Branch struct {
Name string
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

View File

@@ -5,7 +5,6 @@ import (
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
@@ -22,28 +21,22 @@ import (
// BranchListBuilder returns a list of Branch objects for the current repo
type BranchListBuilder struct {
Log *logrus.Entry
GitCommand *GitCommand
Log *logrus.Entry
GitCommand *GitCommand
ReflogCommits []*Commit
}
// NewBranchListBuilder builds a new branch list builder
func NewBranchListBuilder(log *logrus.Entry, gitCommand *GitCommand) (*BranchListBuilder, error) {
func NewBranchListBuilder(log *logrus.Entry, gitCommand *GitCommand, reflogCommits []*Commit) (*BranchListBuilder, error) {
return &BranchListBuilder{
Log: log,
GitCommand: gitCommand,
Log: log,
GitCommand: gitCommand,
ReflogCommits: reflogCommits,
}, nil
}
func (b *BranchListBuilder) obtainCurrentBranchName() string {
branchName, err := b.GitCommand.CurrentBranchName()
if err != nil {
panic(err.Error())
}
return branchName
}
func (b *BranchListBuilder) obtainBranches() []*Branch {
cmdStr := `git branch --format="%(HEAD)|%(refname:short)|%(upstream:short)|%(upstream:track)"`
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)
@@ -51,40 +44,48 @@ func (b *BranchListBuilder) obtainBranches() []*Branch {
trimmedOutput := strings.TrimSpace(output)
outputLines := strings.Split(trimmedOutput, "\n")
branches := make([]*Branch, len(outputLines))
for i, line := range outputLines {
branches := make([]*Branch, 0, len(outputLines))
for _, line := range outputLines {
if line == "" {
continue
}
split := strings.Split(line, SEPARATION_CHAR)
name := split[1]
branches[i] = &Branch{
branch := &Branch{
Name: name,
Pullables: "?",
Pushables: "?",
Head: split[0] == "*",
}
upstreamName := split[2]
if upstreamName == "" {
branches = append(branches, branch)
continue
}
branches[i].UpstreamName = upstreamName
branch.UpstreamName = upstreamName
track := split[3]
re := regexp.MustCompile(`ahead (\d+)`)
match := re.FindStringSubmatch(track)
if len(match) > 1 {
branches[i].Pushables = match[1]
branch.Pushables = match[1]
} else {
branches[i].Pushables = "0"
branch.Pushables = "0"
}
re = regexp.MustCompile(`behind (\d+)`)
match = re.FindStringSubmatch(track)
if len(match) > 1 {
branches[i].Pullables = match[1]
branch.Pullables = match[1]
} else {
branches[i].Pullables = "0"
branch.Pullables = "0"
}
branches = append(branches, branch)
}
return branches
@@ -92,7 +93,6 @@ func (b *BranchListBuilder) obtainBranches() []*Branch {
// Build the list of branches for the current repo
func (b *BranchListBuilder) Build() []*Branch {
currentBranchName := b.obtainCurrentBranchName()
branches := b.obtainBranches()
reflogBranches := b.obtainReflogBranches()
@@ -116,76 +116,45 @@ outer:
branches = append(branchesWithRecency, branches...)
if len(branches) == 0 {
branches = append([]*Branch{{Name: currentBranchName}}, branches...)
}
foundHead := false
for i, branch := range branches {
if branch.Head {
branch.Name = currentBranchName
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
}
// 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) {
// example line: HEAD@{12 minutes ago}|checkout: moving from pulling-from-forks to tim77-patch-1
r := regexp.MustCompile(`HEAD\@\{([^\s]+) ([^\s]+) ago\}\|.*?([^\s]*)$`)
matches := r.FindStringSubmatch(strings.TrimSpace(line))
if len(matches) == 0 {
return "", ""
}
since := matches[1]
unit := matches[2]
branchName := matches[3]
return since + abbreviatedTimeUnit(unit), branchName
}
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]
}
// 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 {
branches := make([]*Branch, 0)
// if we directly put this string in RunCommandWithOutput the compiler complains because it thinks it's a format string
unescaped := "git reflog --date=relative --pretty='%gd|%gs' --grep-reflog='checkout: moving' HEAD"
rawString, err := b.GitCommand.OSCommand.RunCommandWithOutput(unescaped)
if err != nil {
return branches
}
branchNameMap := map[string]bool{}
branchLines := utils.SplitLines(rawString)
for _, line := range branchLines {
recency, branchName := branchInfoFromLine(line)
if branchName == "" {
continue
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,
})
}
}
}
if _, ok := branchNameMap[branchName]; ok {
continue
}
branchNameMap[branchName] = true
branch := &Branch{Name: branchName, Recency: recency}
branches = append(branches, branch)
}
return branches
return reflogBranches
}

View File

@@ -5,11 +5,16 @@ type Commit struct {
Sha string
Name string
Status string // one of "unpushed", "pushed", "merged", "rebasing" or "selected"
DisplayString string
Action string // one of "", "pick", "edit", "squash", "reword", "drop", "fixup"
Copied bool // to know if this commit is ready to be cherry-picked somewhere
Tags []string
ExtraInfo string // something like 'HEAD -> master, tag: v0.15.2'
Author string
Date string
UnixTimestamp int64
}
func (c *Commit) ShortSha() string {
if len(c.Sha) < 8 {
return c.Sha
}
return c.Sha[:8]
}

View File

@@ -4,8 +4,10 @@ import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/fatih/color"
@@ -32,18 +34,16 @@ type CommitListBuilder struct {
OSCommand *OSCommand
Tr *i18n.Localizer
CherryPickedCommits []*Commit
DiffEntries []*Commit
}
// NewCommitListBuilder builds a new commit list builder
func NewCommitListBuilder(log *logrus.Entry, gitCommand *GitCommand, osCommand *OSCommand, tr *i18n.Localizer, cherryPickedCommits []*Commit, diffEntries []*Commit) (*CommitListBuilder, error) {
func NewCommitListBuilder(log *logrus.Entry, gitCommand *GitCommand, osCommand *OSCommand, tr *i18n.Localizer, cherryPickedCommits []*Commit) (*CommitListBuilder, error) {
return &CommitListBuilder{
Log: log,
GitCommand: gitCommand,
OSCommand: osCommand,
Tr: tr,
CherryPickedCommits: cherryPickedCommits,
DiffEntries: diffEntries,
}, nil
}
@@ -55,7 +55,7 @@ func (c *CommitListBuilder) extractCommitFromLine(line string) *Commit {
split := strings.Split(line, SEPARATION_CHAR)
sha := split[0]
date := split[1]
unixTimestamp := split[1]
author := split[2]
extraInfo := strings.TrimSpace(split[3])
message := strings.Join(split[4:], SEPARATION_CHAR)
@@ -69,26 +69,32 @@ func (c *CommitListBuilder) extractCommitFromLine(line string) *Commit {
}
}
unitTimestampInt, _ := strconv.Atoi(unixTimestamp)
return &Commit{
Sha: sha,
Name: message,
DisplayString: line,
Tags: tags,
ExtraInfo: extraInfo,
Date: date,
UnixTimestamp: int64(unitTimestampInt),
Author: author,
}
}
type GetCommitsOptions struct {
Limit bool
FilterPath string
}
// GetCommits obtains the commits of the current branch
func (c *CommitListBuilder) GetCommits(limit bool) ([]*Commit, error) {
func (c *CommitListBuilder) GetCommits(options GetCommitsOptions) ([]*Commit, error) {
commits := []*Commit{}
var rebasingCommits []*Commit
rebaseMode, err := c.GitCommand.RebaseMode()
if err != nil {
return nil, err
}
if rebaseMode != "" {
if rebaseMode != "" && options.FilterPath == "" {
// here we want to also prepend the commits that we're in the process of rebasing
rebasingCommits, err = c.getRebasingCommits(rebaseMode)
if err != nil {
@@ -100,15 +106,20 @@ func (c *CommitListBuilder) GetCommits(limit bool) ([]*Commit, error) {
}
unpushedCommits := c.getUnpushedCommits()
log := c.getLog(limit)
cmd := c.getLogCmd(options)
// now we can split it up and turn it into commits
for _, line := range utils.SplitLines(log) {
err = RunLineOutputCmd(cmd, func(line string) (bool, error) {
commit := c.extractCommitFromLine(line)
_, unpushed := unpushedCommits[commit.Sha[:8]]
_, 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)
@@ -121,19 +132,6 @@ func (c *CommitListBuilder) GetCommits(limit bool) ([]*Commit, error) {
return nil, err
}
commits, err = c.setCommitCherryPickStatuses(commits)
if err != nil {
return nil, err
}
for _, commit := range commits {
for _, entry := range c.DiffEntries {
if entry.Sha == commit.Sha {
commit.Status = "selected"
}
}
}
return commits, nil
}
@@ -267,19 +265,8 @@ func (c *CommitListBuilder) setCommitMergedStatuses(commits []*Commit) ([]*Commi
return commits, nil
}
func (c *CommitListBuilder) setCommitCherryPickStatuses(commits []*Commit) ([]*Commit, error) {
for _, commit := range commits {
for _, cherryPickedCommit := range c.CherryPickedCommits {
if commit.Sha == cherryPickedCommit.Sha {
commit.Copied = true
}
}
}
return commits, nil
}
func (c *CommitListBuilder) getMergeBase() (string, error) {
currentBranch, err := c.GitCommand.CurrentBranchName()
currentBranch, _, err := c.GitCommand.CurrentBranchName()
if err != nil {
return "", err
}
@@ -310,18 +297,16 @@ func (c *CommitListBuilder) getUnpushedCommits() map[string]bool {
}
// getLog gets the git log.
func (c *CommitListBuilder) getLog(limit bool) string {
func (c *CommitListBuilder) getLogCmd(options GetCommitsOptions) *exec.Cmd {
limitFlag := ""
if limit {
limitFlag = "-30"
if options.Limit {
limitFlag = "-300"
}
result, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git log --oneline --pretty=format:\"%%H%s%%ar%s%%aN%s%%d%s%%s\" %s --abbrev=%d", SEPARATION_CHAR, SEPARATION_CHAR, SEPARATION_CHAR, SEPARATION_CHAR, limitFlag, 20))
if err != nil {
// assume if there is an error there are no commits yet for this branch
return ""
filterFlag := ""
if options.FilterPath != "" {
filterFlag = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(options.FilterPath))
}
return result
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))
}

View File

@@ -10,7 +10,7 @@ import (
"github.com/go-errors/errors"
"github.com/jesseduffield/pty"
"github.com/creack/pty"
)
// RunCommandWithOutputLiveWrapper runs a command and return every word that gets written in stdout

View File

@@ -15,12 +15,12 @@ import (
"github.com/go-errors/errors"
gogit "github.com/go-git/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
gitconfig "github.com/tcnksm/go-gitconfig"
gogit "gopkg.in/src-d/go-git.v4"
)
// this takes something like:
@@ -156,9 +156,7 @@ func findDotGitDir(stat func(string) (os.FileInfo, error), readFile func(filenam
return strings.TrimSpace(strings.TrimPrefix(fileContent, "gitdir: ")), nil
}
// GetStashEntries stash entries
func (c *GitCommand) GetStashEntries() []*StashEntry {
// if we directly put this string in RunCommandWithOutput the compiler complains because it thinks it's a format string
func (c *GitCommand) getUnfilteredStashEntries() []*StashEntry {
unescaped := "git stash list --pretty='%gs'"
rawString, _ := c.OSCommand.RunCommandWithOutput(unescaped)
stashEntries := []*StashEntry{}
@@ -168,11 +166,49 @@ func (c *GitCommand) GetStashEntries() []*StashEntry {
return stashEntries
}
// GetStashEntries stash entries
func (c *GitCommand) GetStashEntries(filterPath string) []*StashEntry {
if filterPath == "" {
return c.getUnfilteredStashEntries()
}
unescaped := fmt.Sprintf("git stash list --name-only")
rawString, err := c.OSCommand.RunCommandWithOutput(unescaped)
if err != nil {
return c.getUnfilteredStashEntries()
}
stashEntries := []*StashEntry{}
var currentStashEntry *StashEntry
lines := utils.SplitLines(rawString)
isAStash := func(line string) bool { return strings.HasPrefix(line, "stash@{") }
re := regexp.MustCompile(`stash@\{(\d+)\}`)
outer:
for i := 0; i < len(lines); i++ {
if !isAStash(lines[i]) {
continue
}
match := re.FindStringSubmatch(lines[i])
idx, err := strconv.Atoi(match[1])
if err != nil {
return c.getUnfilteredStashEntries()
}
currentStashEntry = stashEntryFromLine(lines[i], idx)
for i+1 < len(lines) && !isAStash(lines[i+1]) {
i++
if lines[i] == filterPath {
stashEntries = append(stashEntries, currentStashEntry)
continue outer
}
}
}
return stashEntries
}
func stashEntryFromLine(line string, index int) *StashEntry {
return &StashEntry{
Name: line,
Index: index,
DisplayString: line,
Name: line,
Index: index,
}
}
@@ -322,8 +358,8 @@ func (c *GitCommand) Fetch(unamePassQuestion func(string) string, canAskForCrede
}
// ResetToCommit reset to commit
func (c *GitCommand) ResetToCommit(sha string, strength string) error {
return c.OSCommand.RunCommand("git reset --%s %s", strength, sha)
func (c *GitCommand) ResetToCommit(sha string, strength string, options RunCommandOptions) error {
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git reset --%s %s", strength, sha), options)
}
// NewBranch create new branch
@@ -331,19 +367,29 @@ func (c *GitCommand) NewBranch(name string, baseBranch string) error {
return c.OSCommand.RunCommand("git checkout -b %s %s", name, baseBranch)
}
// CurrentBranchName is a function.
func (c *GitCommand) CurrentBranchName() (string, error) {
// CurrentBranchName get the current branch name and displayname.
// the first returned string is the name and the second is the displayname
// e.g. name is 123asdf and displayname is '(HEAD detached at 123asdf)'
func (c *GitCommand) CurrentBranchName() (string, string, error) {
branchName, err := c.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD")
if err != nil || branchName == "HEAD\n" {
output, err := c.OSCommand.RunCommandWithOutput("git branch --contains")
if err != nil {
return "", err
}
re := regexp.MustCompile(CurrentBranchNameRegex)
match := re.FindStringSubmatch(output)
branchName = match[1]
if err == nil && branchName != "HEAD\n" {
trimmedBranchName := strings.TrimSpace(branchName)
return trimmedBranchName, trimmedBranchName, nil
}
return strings.TrimSpace(branchName), nil
output, err := c.OSCommand.RunCommandWithOutput("git branch --contains")
if err != nil {
return "", "", err
}
for _, line := range utils.SplitLines(output) {
re := regexp.MustCompile(CurrentBranchNameRegex)
match := re.FindStringSubmatch(line)
if len(match) > 0 {
branchName = match[1]
displayBranchName := match[0][2:]
return branchName, displayBranchName, nil
}
}
return "HEAD", "HEAD", nil
}
// DeleteBranch delete branch
@@ -474,11 +520,7 @@ func (c *GitCommand) GitStatus() (string, error) {
// IsInMergeState states whether we are still mid-merge
func (c *GitCommand) IsInMergeState() (bool, error) {
output, err := c.OSCommand.RunCommandWithOutput("git status --untracked-files=all")
if err != nil {
return false, err
}
return strings.Contains(output, "conclude merge") || strings.Contains(output, "unmerged paths"), nil
return c.OSCommand.FileExists(fmt.Sprintf("%s/MERGE_HEAD", c.DotGitDir))
}
// RebaseMode returns "" for non-rebase mode, "normal" for normal rebase
@@ -522,12 +564,17 @@ func (c *GitCommand) DiscardUnstagedFileChanges(file *File) error {
}
// Checkout checks out a branch (or commit), with --force if you set the force arg to true
func (c *GitCommand) Checkout(branch string, force bool) error {
type CheckoutOptions struct {
Force bool
EnvVars []string
}
func (c *GitCommand) Checkout(branch string, options CheckoutOptions) error {
forceArg := ""
if force {
if options.Force {
forceArg = "--force "
}
return c.OSCommand.RunCommand("git checkout %s %s", forceArg, branch)
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git checkout %s %s", forceArg, branch), RunCommandOptions{EnvVars: options.EnvVars})
}
// PrepareCommitSubProcess prepares a subprocess for `git commit`
@@ -558,8 +605,12 @@ func (c *GitCommand) Ignore(filename string) error {
return c.OSCommand.AppendLineToFile(".gitignore", filename)
}
func (c *GitCommand) ShowCmdStr(sha string) string {
return fmt.Sprintf("git show --color=%s --no-renames --stat -p %s", c.colorArg(), sha)
func (c *GitCommand) ShowCmdStr(sha string, filterPath string) string {
filterPathArg := ""
if filterPath != "" {
filterPathArg = fmt.Sprintf(" -- %s", c.OSCommand.Quote(filterPath))
}
return fmt.Sprintf("git show --color=%s --no-renames --stat -p %s %s", c.colorArg(), sha, filterPathArg)
}
func (c *GitCommand) GetBranchGraphCmdStr(branchName string) string {
@@ -633,6 +684,7 @@ func (c *GitCommand) RunSkipEditorCommand(command string) error {
cmd.Env = append(
cmd.Env,
"LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY",
"GIT_EDITOR="+lazyGitPath,
"EDITOR="+lazyGitPath,
"VISUAL="+lazyGitPath,
)
@@ -650,7 +702,10 @@ func (c *GitCommand) GenericMerge(commandType string, command string) error {
),
)
if err != nil {
return err
if !strings.Contains(err.Error(), "no rebase in progress") {
return err
}
c.Log.Warn(err)
}
// sometimes we need to do a sequence of things in a rebase but the user needs to
@@ -743,7 +798,7 @@ func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string
)
if overrideEditor {
cmd.Env = append(cmd.Env, "EDITOR="+ex)
cmd.Env = append(cmd.Env, "GIT_EDITOR="+ex)
}
return cmd, nil
@@ -866,7 +921,7 @@ func (c *GitCommand) CherryPickCommits(commits []*Commit) error {
// GetCommitFiles get the specified commit files
func (c *GitCommand) GetCommitFiles(commitSha string, patchManager *PatchManager) ([]*CommitFile, error) {
files, err := c.OSCommand.RunCommandWithOutput("git show --pretty= --name-only --no-renames %s", commitSha)
files, err := c.OSCommand.RunCommandWithOutput("git diff-tree --no-commit-id --name-only -r --no-renames %s", commitSha)
if err != nil {
return nil, err
}
@@ -966,11 +1021,6 @@ func (c *GitCommand) ResetSoft(ref string) error {
return c.OSCommand.RunCommand("git reset --soft " + ref)
}
// DiffCommits show diff between commits
func (c *GitCommand) DiffCommits(sha1, sha2 string) (string, error) {
return c.OSCommand.RunCommandWithOutput("git diff --color=%s --stat -p %s %s", c.colorArg(), sha1, sha2)
}
// CreateFixupCommit creates a commit that fixes up a previous commit
func (c *GitCommand) CreateFixupCommit(sha string) error {
return c.OSCommand.RunCommand("git commit --fixup=%s", sha)
@@ -1105,29 +1155,49 @@ func (c *GitCommand) FetchRemote(remoteName string) error {
return c.OSCommand.RunCommand("git fetch %s", remoteName)
}
func (c *GitCommand) GetReflogCommits() ([]*Commit, error) {
output, err := c.OSCommand.RunCommandWithOutput("git reflog --abbrev=20")
if err != nil {
return nil, err
// GetReflogCommits only returns the new reflog commits since the given lastReflogCommit
// if none is passed (i.e. it's value is nil) then we get all the reflog commits
func (c *GitCommand) GetReflogCommits(lastReflogCommit *Commit, filterPath string) ([]*Commit, bool, error) {
commits := make([]*Commit, 0)
re := regexp.MustCompile(`(\w+).*HEAD@\{([^\}]+)\}: (.*)`)
filterPathArg := ""
if filterPath != "" {
filterPathArg = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(filterPath))
}
lines := strings.Split(strings.TrimSpace(output), "\n")
commits := make([]*Commit, len(lines))
re := regexp.MustCompile(`(\w+).*HEAD@\{\d+\}: (.*)`)
for i, line := range lines {
cmd := c.OSCommand.ExecutableFromString(fmt.Sprintf("git reflog --abbrev=20 --date=unix %s", filterPathArg))
onlyObtainedNewReflogCommits := false
err := RunLineOutputCmd(cmd, func(line string) (bool, error) {
match := re.FindStringSubmatch(line)
if len(match) <= 1 {
continue
return false, nil
}
commits[i] = &Commit{
Sha: match[1],
Name: match[2],
Status: "reflog",
unixTimestamp, _ := strconv.Atoi(match[2])
commit := &Commit{
Sha: match[1],
Name: match[3],
UnixTimestamp: int64(unixTimestamp),
Status: "reflog",
}
if lastReflogCommit != nil && commit.Sha == lastReflogCommit.Sha && commit.UnixTimestamp == lastReflogCommit.UnixTimestamp {
onlyObtainedNewReflogCommits = true
// after this point we already have these reflogs loaded so we'll simply return the new ones
return true, nil
}
commits = append(commits, commit)
return false, nil
})
if err != nil {
return nil, false, err
}
return commits, nil
return commits, onlyObtainedNewReflogCommits, nil
}
func (c *GitCommand) ConfiguredPager() string {
@@ -1167,3 +1237,15 @@ func (c *GitCommand) colorArg() string {
func (c *GitCommand) RenameBranch(oldName string, newName string) error {
return c.OSCommand.RunCommand("git branch --move %s %s", oldName, newName)
}
func (c *GitCommand) WorkingTreeState() string {
rebaseMode, _ := c.RebaseMode()
if rebaseMode != "" {
return "rebasing"
}
merging, _ := c.IsInMergeState()
if merging {
return "merging"
}
return "normal"
}

View File

@@ -10,10 +10,10 @@ import (
"time"
"github.com/go-errors/errors"
gogit "github.com/go-git/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/test"
"github.com/stretchr/testify/assert"
gogit "gopkg.in/src-d/go-git.v4"
)
type fileInfoMock struct {
@@ -294,12 +294,10 @@ func TestGitCommandGetStashEntries(t *testing.T) {
{
0,
"WIP on add-pkg-commands-test: 55c6af2 increase parallel build",
"WIP on add-pkg-commands-test: 55c6af2 increase parallel build",
},
{
1,
"WIP on master: bb86a3f update github template",
"WIP on master: bb86a3f update github template",
},
}
@@ -314,7 +312,7 @@ func TestGitCommandGetStashEntries(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = s.command
s.test(gitCmd.GetStashEntries())
s.test(gitCmd.GetStashEntries(""))
})
}
}
@@ -628,7 +626,7 @@ func TestGitCommandResetToCommit(t *testing.T) {
return exec.Command("echo")
}
assert.NoError(t, gitCmd.ResetToCommit("78976bc", "hard"))
assert.NoError(t, gitCmd.ResetToCommit("78976bc", "hard", RunCommandOptions{}))
}
// TestGitCommandNewBranch is a function.
@@ -1099,75 +1097,6 @@ func TestGitCommandUnstageFile(t *testing.T) {
}
}
// TestGitCommandIsInMergeState is a function.
func TestGitCommandIsInMergeState(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(bool, error)
}
scenarios := []scenario{
{
"An error occurred when running status command",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"status", "--untracked-files=all"}, args)
return exec.Command("test")
},
func(isInMergeState bool, err error) {
assert.Error(t, err)
assert.False(t, isInMergeState)
},
},
{
"Is not in merge state",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"status", "--untracked-files=all"}, args)
return exec.Command("echo")
},
func(isInMergeState bool, err error) {
assert.False(t, isInMergeState)
assert.NoError(t, err)
},
},
{
"Command output contains conclude merge",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"status", "--untracked-files=all"}, args)
return exec.Command("echo", "'conclude merge'")
},
func(isInMergeState bool, err error) {
assert.True(t, isInMergeState)
assert.NoError(t, err)
},
},
{
"Command output contains unmerged paths",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"status", "--untracked-files=all"}, args)
return exec.Command("echo", "'unmerged paths'")
},
func(isInMergeState bool, err error) {
assert.True(t, isInMergeState)
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = s.command
s.test(gitCmd.IsInMergeState())
})
}
}
// TestGitCommandDiscardAllFileChanges is a function.
func TestGitCommandDiscardAllFileChanges(t *testing.T) {
type scenario struct {
@@ -1439,7 +1368,7 @@ func TestGitCommandCheckout(t *testing.T) {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.command = s.command
s.test(gitCmd.Checkout("test", s.force))
s.test(gitCmd.Checkout("test", CheckoutOptions{Force: s.force}))
})
}
}
@@ -1549,7 +1478,7 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(string, error)
test func(string, string, error)
}
scenarios := []scenario{
@@ -1559,9 +1488,10 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
assert.Equal(t, "git", cmd)
return exec.Command("echo", "master")
},
func(output string, err error) {
func(name string, displayname string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "master", output)
assert.EqualValues(t, "master", name)
assert.EqualValues(t, "master", displayname)
},
},
{
@@ -1580,9 +1510,32 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
return nil
},
func(output string, err error) {
func(name string, displayname string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "master", output)
assert.EqualValues(t, "master", name)
assert.EqualValues(t, "master", displayname)
},
},
{
"handles a detached head",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("test")
case "branch":
assert.EqualValues(t, []string{"branch", "--contains"}, args)
return exec.Command("echo", "* (HEAD detached at 123abcd)")
}
return nil
},
func(name string, displayname string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "123abcd", name)
assert.EqualValues(t, "(HEAD detached at 123abcd)", displayname)
},
},
{
@@ -1591,9 +1544,10 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
assert.Equal(t, "git", cmd)
return exec.Command("test")
},
func(output string, err error) {
func(name string, displayname string, err error) {
assert.Error(t, err)
assert.EqualValues(t, "", output)
assert.EqualValues(t, "", name)
assert.EqualValues(t, "", displayname)
},
},
}
@@ -1907,7 +1861,7 @@ func TestGitCommandGetCommitFiles(t *testing.T) {
"123456",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: "git show --pretty= --name-only --no-renames 123456",
Expect: "git diff-tree --no-commit-id --name-only -r --no-renames 123456",
Replace: "echo 'hello\nworld'",
},
}),
@@ -2122,6 +2076,13 @@ func TestGitCommandSkipEditorCommand(t *testing.T) {
"expected EDITOR to be set for a non-interactive external command",
)
test.AssertContainsMatch(
t,
cmd.Env,
regexp.MustCompile("^GIT_EDITOR="),
"expected GIT_EDITOR to be set for a non-interactive external command",
)
test.AssertContainsMatch(
t,
cmd.Env,

View File

@@ -1,6 +1,7 @@
package commands
import (
"bufio"
"fmt"
"io/ioutil"
"os"
@@ -64,6 +65,22 @@ func (c *OSCommand) SetBeforeExecuteCmd(cmd func(*exec.Cmd)) {
c.beforeExecuteCmd = cmd
}
type RunCommandOptions struct {
EnvVars []string
}
func (c *OSCommand) RunCommandWithOutputWithOptions(command string, options RunCommandOptions) (string, error) {
c.Log.WithField("command", command).Info("RunCommand")
cmd := c.ExecutableFromString(command)
cmd.Env = append(cmd.Env, options.EnvVars...)
return sanitisedCommandOutput(cmd.CombinedOutput())
}
func (c *OSCommand) RunCommandWithOptions(command string, options RunCommandOptions) error {
_, err := c.RunCommandWithOutputWithOptions(command, options)
return err
}
// RunCommandWithOutput wrapper around commands returning their output and error
// NOTE: If you don't pass any formatArgs we'll just use the command directly,
// however there's a bizarre compiler error/warning when you pass in a formatString
@@ -409,3 +426,31 @@ func Kill(cmd *exec.Cmd) error {
}
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
}

View File

@@ -131,14 +131,22 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*Commit, sourceCommitId
return c.GenericMerge("rebase", "continue")
}
func (c *GitCommand) PullPatchIntoIndex(commits []*Commit, commitIdx int, p *PatchManager) error {
func (c *GitCommand) PullPatchIntoIndex(commits []*Commit, commitIdx int, p *PatchManager, stash bool) error {
if stash {
if err := c.StashSave(c.Tr.SLocalize("StashPrefix") + commits[commitIdx].Sha); err != nil {
return err
}
}
if err := c.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil {
return err
}
if err := p.ApplyPatches(true); err != nil {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
if c.WorkingTreeState() == "rebasing" {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
}
}
return err
}
@@ -155,12 +163,20 @@ func (c *GitCommand) PullPatchIntoIndex(commits []*Commit, commitIdx int, p *Pat
c.onSuccessfulContinue = func() error {
// add patches to index
if err := p.ApplyPatches(false); err != nil {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
if c.WorkingTreeState() == "rebasing" {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
}
}
return err
}
if stash {
if err := c.StashDo(0, "apply"); err != nil {
return err
}
}
c.PatchManager.Reset()
return nil
}

View File

@@ -1,24 +1,8 @@
package commands
import (
"fmt"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// Remote : A git remote
type Remote struct {
Name string
Urls []string
Selected bool
Branches []*RemoteBranch
}
// GetDisplayStrings returns the display string of a remote
func (r *Remote) GetDisplayStrings(isFocused bool) []string {
branchCount := len(r.Branches)
return []string{r.Name, utils.ColoredString(fmt.Sprintf("%d branches", branchCount), color.FgBlue)}
}

View File

@@ -5,3 +5,7 @@ type RemoteBranch struct {
Name string
RemoteName string
}
func (r *RemoteBranch) FullName() string {
return r.RemoteName + "/" + r.Name
}

View File

@@ -1,13 +1,13 @@
package commands
import "fmt"
// StashEntry : A git stash entry
type StashEntry struct {
Index int
Name string
DisplayString string
Index int
Name string
}
// GetDisplayStrings returns the display string of branch
func (s *StashEntry) GetDisplayStrings(isFocused bool) []string {
return []string{s.DisplayString}
func (s *StashEntry) RefName() string {
return fmt.Sprintf("stash@{%d}", s.Index)
}

View File

@@ -283,6 +283,10 @@ keybinding:
nextItem: '<down>'
prevItem-alt: 'k'
nextItem-alt: 'j'
prevPage: ','
nextPage: '.'
gotoTop: '<'
gotoBottom: '>'
prevBlock: '<left>'
nextBlock: '<right>'
prevBlock-alt: 'h'
@@ -314,6 +318,10 @@ keybinding:
prevTab: '['
nextScreenMode: '+'
prevScreenMode: '_'
undo: 'z'
redo: '<c-z>'
filteringMenu: <c-s>
diffingMenu: '<c-e>'
status:
checkForUpdate: 'u'
recentRepos: '<enter>'
@@ -358,8 +366,8 @@ keybinding:
cherryPickCopyRange: 'C'
pasteCommits: 'v'
tagCommit: 'T'
toggleDiffCommit: 'i'
checkoutCommit: '<space>'
resetCherryPick: '<c-R>'
stash:
popStash: 'g'
commitFiles:
@@ -369,7 +377,6 @@ keybinding:
toggleDragSelect-alt: 'V'
toggleSelectHunk: 'a'
pickBothHunks: 'b'
undo: 'z'
`)
}

View File

@@ -72,7 +72,7 @@ func (gui *Gui) WithWaitingStatus(name string, f func() error) error {
if err := f(); err != nil {
gui.g.Update(func(g *gocui.Gui) error {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
})
}
}()

View File

@@ -42,6 +42,10 @@ func (gui *Gui) handleBranchSelect(g *gocui.Gui, v *gocui.View) error {
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),
)
@@ -53,43 +57,43 @@ func (gui *Gui) handleBranchSelect(g *gocui.Gui, v *gocui.View) error {
// gui.refreshStatus is called at the end of this because that's when we can
// be sure there is a state.Branches array to pick the current branch from
func (gui *Gui) refreshBranches(g *gocui.Gui) error {
if err := gui.refreshRemotes(); err != nil {
return err
}
if err := gui.refreshTags(); err != nil {
return err
}
g.Update(func(g *gocui.Gui) error {
builder, err := commands.NewBranchListBuilder(gui.Log, gui.GitCommand)
func (gui *Gui) refreshBranches() {
reflogCommits := gui.State.FilteredReflogCommits
if gui.inFilterMode() {
// in filter mode we filter our reflog commits to just those containing the path
// however we need all the reflog entries to populate the recencies of our branches
// which allows us to order them correctly. So if we're filtering we'll just
// manually load all the reflog commits here
var err error
reflogCommits, _, err = gui.GitCommand.GetReflogCommits(nil, "")
if err != nil {
return err
gui.Log.Error(err)
}
gui.State.Branches = builder.Build()
}
// TODO: if we're in the remotes view and we've just deleted a remote we need to refresh accordingly
if gui.getBranchesView().Context == "local-branches" {
if err := gui.renderLocalBranchesWithSelection(); err != nil {
return err
}
}
builder, err := commands.NewBranchListBuilder(gui.Log, gui.GitCommand, reflogCommits)
if err != nil {
_ = gui.surfaceError(err)
}
gui.State.Branches = builder.Build()
return gui.refreshStatus(g)
})
return nil
// TODO: if we're in the remotes view and we've just deleted a remote we need to refresh accordingly
if gui.getBranchesView().Context == "local-branches" {
_ = gui.renderLocalBranchesWithSelection()
}
gui.refreshStatus()
}
func (gui *Gui) renderLocalBranchesWithSelection() error {
branchesView := gui.getBranchesView()
gui.refreshSelectedLine(&gui.State.Panels.Branches.SelectedLine, len(gui.State.Branches))
displayStrings := presentation.GetBranchListDisplayStrings(gui.State.Branches, gui.State.ScreenMode != SCREEN_NORMAL)
displayStrings := presentation.GetBranchListDisplayStrings(gui.State.Branches, gui.State.ScreenMode != SCREEN_NORMAL, gui.State.Diff.Ref)
gui.renderDisplayStrings(branchesView, displayStrings)
if gui.g.CurrentView() == branchesView {
if err := gui.handleBranchSelect(gui.g, branchesView); err != nil {
return err
return gui.surfaceError(err)
}
}
@@ -103,10 +107,10 @@ func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error {
return nil
}
if gui.State.Panels.Branches.SelectedLine == 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("AlreadyCheckedOutBranch"))
return gui.createErrorPanel(gui.Tr.SLocalize("AlreadyCheckedOutBranch"))
}
branch := gui.getSelectedBranch()
return gui.handleCheckoutRef(branch.Name)
return gui.handleCheckoutRef(branch.Name, handleCheckoutRefOptions{})
}
func (gui *Gui) handleCreatePullRequestPress(g *gocui.Gui, v *gocui.View) error {
@@ -114,7 +118,7 @@ func (gui *Gui) handleCreatePullRequestPress(g *gocui.Gui, v *gocui.View) error
branch := gui.getSelectedBranch()
if err := pullRequest.Create(branch); err != nil {
return gui.createErrorPanel(g, err.Error())
return gui.surfaceError(err)
}
return nil
@@ -136,53 +140,72 @@ func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
message := gui.Tr.SLocalize("SureForceCheckout")
title := gui.Tr.SLocalize("ForceCheckoutBranch")
return gui.createConfirmationPanel(g, v, true, title, message, func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.Checkout(branch.Name, true); err != nil {
_ = gui.createErrorPanel(g, err.Error())
if err := gui.GitCommand.Checkout(branch.Name, commands.CheckoutOptions{Force: true}); err != nil {
_ = gui.surfaceError(err)
}
return gui.refreshSidePanels(g)
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}, nil)
}
func (gui *Gui) handleCheckoutRef(ref string) error {
if err := gui.GitCommand.Checkout(ref, false); err != nil {
// note, this will only work for english-language git commands. If we force git to use english, and the error isn't this one, then the user will receive an english command they may not understand. I'm not sure what the best solution to this is. Running the command once in english and a second time in the native language is one option
type handleCheckoutRefOptions struct {
WaitingStatus string
EnvVars []string
}
if strings.Contains(err.Error(), "Please commit your changes or stash them before you switch branch") {
// offer to autostash changes
return gui.createConfirmationPanel(gui.g, gui.getBranchesView(), true, gui.Tr.SLocalize("AutoStashTitle"), gui.Tr.SLocalize("AutoStashPrompt"), func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.StashSave(gui.Tr.SLocalize("StashPrefix") + ref); err != nil {
return gui.createErrorPanel(g, err.Error())
}
if err := gui.GitCommand.Checkout(ref, false); err != nil {
return gui.createErrorPanel(g, err.Error())
}
// checkout successful so we select the new branch
gui.State.Panels.Branches.SelectedLine = 0
if err := gui.GitCommand.StashDo(0, "pop"); err != nil {
if err := gui.refreshSidePanels(g); err != nil {
return err
}
return gui.createErrorPanel(g, err.Error())
}
return gui.refreshSidePanels(g)
}, nil)
}
if err := gui.createErrorPanel(gui.g, err.Error()); err != nil {
return err
}
func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions) error {
waitingStatus := options.WaitingStatus
if waitingStatus == "" {
waitingStatus = gui.Tr.SLocalize("CheckingOutStatus")
}
gui.State.Panels.Branches.SelectedLine = 0
gui.State.Panels.Commits.SelectedLine = 0
return gui.refreshSidePanels(gui.g)
cmdOptions := commands.CheckoutOptions{Force: false, EnvVars: options.EnvVars}
onSuccess := func() {
gui.State.Panels.Branches.SelectedLine = 0
gui.State.Panels.Commits.SelectedLine = 0
// loading a heap of commits is slow so we limit them whenever doing a reset
gui.State.Panels.Commits.LimitCommits = true
}
return gui.WithWaitingStatus(waitingStatus, func() error {
if err := gui.GitCommand.Checkout(ref, cmdOptions); err != nil {
// note, this will only work for english-language git commands. If we force git to use english, and the error isn't this one, then the user will receive an english command they may not understand. I'm not sure what the best solution to this is. Running the command once in english and a second time in the native language is one option
if 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))
return gui.handleCheckoutRef(gui.trimmedContent(v), handleCheckoutRefOptions{})
})
}
@@ -207,12 +230,10 @@ func (gui *Gui) handleNewBranch(g *gocui.Gui, v *gocui.View) error {
)
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.createErrorPanel(g, err.Error())
return gui.surfaceError(err)
}
if err := gui.refreshSidePanels(g); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return gui.handleBranchSelect(g, v)
gui.State.Panels.Branches.SelectedLine = 0
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
})
}
@@ -227,7 +248,7 @@ func (gui *Gui) deleteBranch(g *gocui.Gui, v *gocui.View, force bool) error {
}
checkedOutBranch := gui.getCheckedOutBranch()
if checkedOutBranch.Name == selectedBranch.Name {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantDeleteCheckOutBranch"))
return gui.createErrorPanel(gui.Tr.SLocalize("CantDeleteCheckOutBranch"))
}
return gui.deleteNamedBranch(g, v, selectedBranch, force)
}
@@ -252,19 +273,23 @@ func (gui *Gui) deleteNamedBranch(g *gocui.Gui, v *gocui.View, selectedBranch *c
if !force && strings.Contains(errMessage, "is not fully merged") {
return gui.deleteNamedBranch(g, v, selectedBranch, true)
}
return gui.createErrorPanel(g, errMessage)
return gui.createErrorPanel(errMessage)
}
return gui.refreshSidePanels(g)
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{BRANCHES}})
}, nil)
}
func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
if gui.GitCommand.IsHeadDetached() {
return gui.createErrorPanel(gui.g, "Cannot merge branch in detached head state. You might have checked out a commit directly or a remote branch, in which case you should checkout the local branch you want to be on")
return gui.createErrorPanel("Cannot merge branch in detached head state. You might have checked out a commit directly or a remote branch, in which case you should checkout the local branch you want to be on")
}
checkedOutBranchName := gui.getCheckedOutBranch().Name
if checkedOutBranchName == branchName {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("CantMergeBranchIntoItself"))
return gui.createErrorPanel(gui.Tr.SLocalize("CantMergeBranchIntoItself"))
}
prompt := gui.Tr.TemplateLocalize(
"ConfirmMerge",
@@ -282,6 +307,10 @@ func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error {
}
func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
selectedBranchName := gui.getSelectedBranch().Name
return gui.mergeBranchIntoCheckedOutBranch(selectedBranchName)
}
@@ -292,9 +321,13 @@ func (gui *Gui) handleRebaseOntoLocalBranch(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleRebaseOntoBranch(selectedBranchName string) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
checkedOutBranch := gui.getCheckedOutBranch().Name
if selectedBranchName == checkedOutBranch {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("CantRebaseOntoSelf"))
return gui.createErrorPanel(gui.Tr.SLocalize("CantRebaseOntoSelf"))
}
prompt := gui.Tr.TemplateLocalize(
"ConfirmRebase",
@@ -319,15 +352,15 @@ func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
return nil
}
if branch.Pushables == "?" {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("FwdNoUpstream"))
return gui.createErrorPanel(gui.Tr.SLocalize("FwdNoUpstream"))
}
if branch.Pushables != "0" {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("FwdCommitsToPush"))
return gui.createErrorPanel(gui.Tr.SLocalize("FwdCommitsToPush"))
}
upstream, err := gui.GitCommand.GetUpstreamForBranch(branch.Name)
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
split := strings.Split(upstream, "/")
@@ -346,14 +379,16 @@ func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error {
if gui.State.Panels.Branches.SelectedLine == 0 {
if err := gui.GitCommand.PullWithoutPasswordCheck("--ff-only"); err != nil {
_ = gui.createErrorPanel(gui.g, err.Error())
_ = gui.surfaceError(err)
return
}
_ = gui.refreshSidePanels(gui.g)
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC})
} else {
if err := gui.GitCommand.FastForward(branch.Name, remoteName, remoteBranchName); err != nil {
_ = gui.createErrorPanel(gui.g, err.Error())
_ = gui.surfaceError(err)
return
}
_ = gui.refreshBranches(gui.g)
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{BRANCHES}})
}
_ = gui.closeConfirmationPrompt(gui.g, true)
@@ -448,19 +483,22 @@ func (gui *Gui) handleRenameBranch(g *gocui.Gui, v *gocui.View) error {
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.createErrorPanel(gui.g, err.Error())
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, false); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
if err := gui.GitCommand.Checkout(newName, commands.CheckoutOptions{Force: false}); err != nil {
return gui.surfaceError(err)
}
return gui.refreshBranches(gui.g)
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
})
}
@@ -477,5 +515,8 @@ func (gui *Gui) handleRenameBranch(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) currentBranch() *commands.Branch {
if len(gui.State.Branches) == 0 {
return nil
}
return gui.State.Branches[0]
}

View File

@@ -7,7 +7,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
)
func (gui *Gui) getSelectedCommitFile(g *gocui.Gui) *commands.CommitFile {
func (gui *Gui) getSelectedCommitFile() *commands.CommitFile {
selectedLine := gui.State.Panels.CommitFiles.SelectedLine
if selectedLine == -1 {
return nil
@@ -34,7 +34,7 @@ func (gui *Gui) handleCommitFileSelect(g *gocui.Gui, v *gocui.View) error {
gui.handleEscapeLineByLinePanel()
}
commitFile := gui.getSelectedCommitFile(g)
commitFile := gui.getSelectedCommitFile()
if commitFile == nil {
gui.renderString(g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
return nil
@@ -64,10 +64,10 @@ func (gui *Gui) handleCheckoutCommitFile(g *gocui.Gui, v *gocui.View) error {
file := gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLine]
if err := gui.GitCommand.CheckoutFile(file.Sha, file.Name); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
return gui.refreshFiles()
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}
func (gui *Gui) handleDiscardOldFileChange(g *gocui.Gui, v *gocui.View) error {
@@ -85,7 +85,7 @@ func (gui *Gui) handleDiscardOldFileChange(g *gocui.Gui, v *gocui.View) error {
}
}
return gui.refreshSidePanels(gui.g)
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI})
})
}, nil)
}
@@ -99,28 +99,28 @@ func (gui *Gui) refreshCommitFilesView() error {
return err
}
commit := gui.getSelectedCommit(gui.g)
commit := gui.getSelectedCommit()
if commit == nil {
return nil
}
files, err := gui.GitCommand.GetCommitFiles(commit.Sha, gui.GitCommand.PatchManager)
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
gui.State.CommitFiles = files
gui.refreshSelectedLine(&gui.State.Panels.CommitFiles.SelectedLine, len(gui.State.CommitFiles))
commitsFileView := gui.getCommitFilesView()
displayStrings := presentation.GetCommitFileListDisplayStrings(gui.State.CommitFiles)
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(g)
file := gui.getSelectedCommitFile()
return gui.openFile(file.Name)
}
@@ -129,7 +129,7 @@ func (gui *Gui) handleToggleFileForPatch(g *gocui.Gui, v *gocui.View) error {
return err
}
commitFile := gui.getSelectedCommitFile(g)
commitFile := gui.getSelectedCommitFile()
if commitFile == nil {
gui.renderString(g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
return nil
@@ -167,7 +167,7 @@ func (gui *Gui) startPatchManager() error {
diffMap[commitFile.Name] = commitText
}
commit := gui.getSelectedCommit(gui.g)
commit := gui.getSelectedCommit()
if commit == nil {
return errors.New("No commit selected")
}
@@ -185,7 +185,7 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
return err
}
commitFile := gui.getSelectedCommitFile(gui.g)
commitFile := gui.getSelectedCommitFile()
if commitFile == nil {
gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
return nil
@@ -209,7 +209,9 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
return gui.createConfirmationPanel(gui.g, gui.getCommitFilesView(), false, gui.Tr.SLocalize("DiscardPatch"), gui.Tr.SLocalize("DiscardPatchConfirm"), func(g *gocui.Gui, v *gocui.View) error {
gui.GitCommand.PatchManager.Reset()
return enterTheFile(selectedLineIdx)
}, nil)
}, func(g *gocui.Gui, v *gocui.View) error {
return gui.switchFocus(gui.g, nil, gui.getCommitFilesView())
})
}
return enterTheFile(selectedLineIdx)

View File

@@ -15,7 +15,7 @@ import (
func (gui *Gui) runSyncOrAsyncCommand(sub *exec.Cmd, err error) (bool, error) {
if err != nil {
if err != gui.Errors.ErrSubProcess {
return false, gui.createErrorPanel(gui.g, err.Error())
return false, gui.surfaceError(err)
}
}
if sub != nil {
@@ -28,7 +28,7 @@ func (gui *Gui) runSyncOrAsyncCommand(sub *exec.Cmd, err error) (bool, error) {
func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
message := gui.trimmedContent(v)
if message == "" {
return gui.createErrorPanel(g, gui.Tr.SLocalize("CommitWithoutMessageErr"))
return gui.createErrorPanel(gui.Tr.SLocalize("CommitWithoutMessageErr"))
}
flags := ""
skipHookPrefix := gui.Config.GetUserConfig().GetString("git.skipHookPrefix")
@@ -48,7 +48,7 @@ func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
_ = v.SetOrigin(0, 0)
_, _ = g.SetViewOnBottom("commitMessage")
_ = gui.switchFocus(g, v, gui.getFilesView())
return gui.refreshSidePanels(g)
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}
func (gui *Gui) handleCommitClose(g *gocui.Gui, v *gocui.View) error {

View File

@@ -2,6 +2,7 @@ package gui
import (
"strconv"
"sync"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
@@ -11,7 +12,7 @@ import (
// list panel functions
func (gui *Gui) getSelectedCommit(g *gocui.Gui) *commands.Commit {
func (gui *Gui) getSelectedCommit() *commands.Commit {
selectedLine := gui.State.Panels.Commits.SelectedLine
if selectedLine == -1 {
return nil
@@ -35,11 +36,11 @@ func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
}
state := gui.State.Panels.Commits
if state.SelectedLine > 20 && state.LimitCommits {
if state.SelectedLine > 290 && state.LimitCommits {
state.LimitCommits = false
go func() {
if err := gui.refreshCommitsWithLimit(); err != nil {
_ = gui.createErrorPanel(gui.g, err.Error())
_ = gui.surfaceError(err)
}
}()
}
@@ -48,20 +49,19 @@ func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
gui.getSecondaryView().Title = "Custom Patch"
gui.handleEscapeLineByLinePanel()
commit := gui.getSelectedCommit(g)
commit := gui.getSelectedCommit()
if commit == nil {
return gui.newStringTask("main", gui.Tr.SLocalize("NoCommitsThisBranch"))
}
v.FocusPoint(0, gui.State.Panels.Commits.SelectedLine)
// if specific diff mode is on, don't show diff
if gui.State.Panels.Commits.SpecificDiffMode {
return nil
if gui.inDiffMode() {
return gui.renderDiff()
}
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowCmdStr(commit.Sha),
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.FilterPath),
)
if err := gui.newPtyTask("main", cmd); err != nil {
gui.Log.Error(err)
@@ -70,37 +70,58 @@ func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
return nil
}
func (gui *Gui) refreshCommits(g *gocui.Gui) error {
g.Update(func(*gocui.Gui) error {
// I think this is here for the sake of some kind of rebasing thing
_ = gui.refreshStatus(g)
if err := gui.refreshCommitsWithLimit(); err != nil {
return err
}
// doing this async because it shouldn't hold anything up
// 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() {
if err := gui.refreshReflogCommits(); err != nil {
_ = gui.createErrorPanel(gui.g, err.Error())
}
_ = gui.refreshReflogCommits()
gui.refreshBranches()
gui.State.StartupStage = COMPLETE
}()
if g.CurrentView() == gui.getCommitFilesView() || (g.CurrentView() == gui.getMainView() || gui.State.MainContext == "patch-building") {
return gui.refreshCommitFilesView()
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()
}
return nil
})
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, gui.State.DiffEntries)
builder, err := commands.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr, gui.State.CherryPickedCommits)
if err != nil {
return err
}
commits, err := builder.GetCommits(gui.State.Panels.Commits.LimitCommits)
commits, err := builder.GetCommits(commands.GetCommitsOptions{Limit: gui.State.Panels.Commits.LimitCommits, FilterPath: gui.State.FilterPath})
if err != nil {
return err
}
@@ -118,8 +139,12 @@ func (gui *Gui) refreshCommitsWithLimit() error {
// specific functions
func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
if len(gui.State.Commits) <= 1 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash"))
return gui.createErrorPanel(gui.Tr.SLocalize("YouNoCommitsToSquash"))
}
applied, err := gui.handleMidRebaseCommand("squash")
@@ -139,8 +164,12 @@ func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
if len(gui.State.Commits) <= 1 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("YouNoCommitsToSquash"))
return gui.createErrorPanel(gui.Tr.SLocalize("YouNoCommitsToSquash"))
}
applied, err := gui.handleMidRebaseCommand("fixup")
@@ -160,6 +189,10 @@ func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
applied, err := gui.handleMidRebaseCommand("reword")
if err != nil {
return err
@@ -169,20 +202,22 @@ func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
}
if gui.State.Panels.Commits.SelectedLine != 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("OnlyRenameTopCommit"))
return gui.createErrorPanel(gui.Tr.SLocalize("OnlyRenameTopCommit"))
}
return gui.createPromptPanel(g, v, gui.Tr.SLocalize("renameCommit"), "", func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.RenameCommit(v.Buffer()); err != nil {
return gui.createErrorPanel(g, err.Error())
return gui.surfaceError(err)
}
if err := gui.refreshCommits(g); err != nil {
panic(err)
}
return gui.handleCommitSelect(g, v)
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
})
}
func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
applied, err := gui.handleMidRebaseCommand("reword")
if err != nil {
return err
@@ -193,7 +228,7 @@ func (gui *Gui) handleRenameCommitEditor(g *gocui.Gui, v *gocui.View) error {
subProcess, err := gui.GitCommand.RewordCommit(gui.State.Commits, gui.State.Panels.Commits.SelectedLine)
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
if subProcess != nil {
gui.SubProcess = subProcess
@@ -217,16 +252,22 @@ func (gui *Gui) handleMidRebaseCommand(action string) (bool, error) {
// our input or we set a lazygit client as the EDITOR env variable and have it
// request us to edit the commit message when prompted.
if action == "reword" {
return true, gui.createErrorPanel(gui.g, gui.Tr.SLocalize("rewordNotSupported"))
return true, gui.createErrorPanel(gui.Tr.SLocalize("rewordNotSupported"))
}
if err := gui.GitCommand.EditRebaseTodo(gui.State.Panels.Commits.SelectedLine, action); err != nil {
return false, gui.createErrorPanel(gui.g, err.Error())
return false, gui.surfaceError(err)
}
return true, gui.refreshCommits(gui.g)
// TODO: consider doing this in a way that is less expensive. We don't actually
// need to reload all the commits, just the TODO commits.
return true, gui.refreshSidePanels(refreshOptions{scope: []int{COMMITS}})
}
func (gui *Gui) handleCommitDelete(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
applied, err := gui.handleMidRebaseCommand("drop")
if err != nil {
return err
@@ -244,6 +285,10 @@ func (gui *Gui) handleCommitDelete(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleCommitMoveDown(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
index := gui.State.Panels.Commits.SelectedLine
selectedCommit := gui.State.Commits[index]
if selectedCommit.Status == "rebasing" {
@@ -251,10 +296,10 @@ func (gui *Gui) handleCommitMoveDown(g *gocui.Gui, v *gocui.View) error {
return nil
}
if err := gui.GitCommand.MoveTodoDown(index); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
gui.State.Panels.Commits.SelectedLine++
return gui.refreshCommits(gui.g)
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI, scope: []int{COMMITS, BRANCHES}})
}
return gui.WithWaitingStatus(gui.Tr.SLocalize("MovingStatus"), func() error {
@@ -267,6 +312,10 @@ func (gui *Gui) handleCommitMoveDown(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleCommitMoveUp(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
index := gui.State.Panels.Commits.SelectedLine
if index == 0 {
return nil
@@ -274,10 +323,10 @@ func (gui *Gui) handleCommitMoveUp(g *gocui.Gui, v *gocui.View) error {
selectedCommit := gui.State.Commits[index]
if selectedCommit.Status == "rebasing" {
if err := gui.GitCommand.MoveTodoDown(index - 1); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
gui.State.Panels.Commits.SelectedLine--
return gui.refreshCommits(gui.g)
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI, scope: []int{COMMITS, BRANCHES}})
}
return gui.WithWaitingStatus(gui.Tr.SLocalize("MovingStatus"), func() error {
@@ -290,6 +339,10 @@ func (gui *Gui) handleCommitMoveUp(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleCommitEdit(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
applied, err := gui.handleMidRebaseCommand("edit")
if err != nil {
return err
@@ -305,6 +358,10 @@ func (gui *Gui) handleCommitEdit(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleCommitAmendTo(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
return gui.createConfirmationPanel(gui.g, v, true, gui.Tr.SLocalize("AmendCommitTitle"), gui.Tr.SLocalize("AmendCommitPrompt"), func(*gocui.Gui, *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("AmendingStatus"), func() error {
err := gui.GitCommand.AmendTo(gui.State.Commits[gui.State.Panels.Commits.SelectedLine].Sha)
@@ -314,6 +371,10 @@ func (gui *Gui) handleCommitAmendTo(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleCommitPick(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
applied, err := gui.handleMidRebaseCommand("pick")
if err != nil {
return err
@@ -328,14 +389,22 @@ func (gui *Gui) handleCommitPick(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleCommitRevert(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
if err := gui.GitCommand.Revert(gui.State.Commits[gui.State.Panels.Commits.SelectedLine].Sha); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
gui.State.Panels.Commits.SelectedLine++
return gui.refreshCommits(gui.g)
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI, scope: []int{COMMITS, BRANCHES}})
}
func (gui *Gui) handleCopyCommit(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
// get currently selected commit, add the sha to state.
commit := gui.State.Commits[gui.State.Panels.Commits.SelectedLine]
@@ -343,22 +412,29 @@ func (gui *Gui) handleCopyCommit(g *gocui.Gui, v *gocui.View) error {
for index, cherryPickedCommit := range gui.State.CherryPickedCommits {
if commit.Sha == cherryPickedCommit.Sha {
gui.State.CherryPickedCommits = append(gui.State.CherryPickedCommits[0:index], gui.State.CherryPickedCommits[index+1:]...)
return gui.refreshCommits(gui.g)
return gui.renderBranchCommitsWithSelection()
}
}
gui.addCommitToCherryPickedCommits(gui.State.Panels.Commits.SelectedLine)
return gui.refreshCommits(gui.g)
return gui.renderBranchCommitsWithSelection()
}
func (gui *Gui) cherryPickedCommitShaMap() map[string]bool {
commitShaMap := map[string]bool{}
for _, commit := range gui.State.CherryPickedCommits {
commitShaMap[commit.Sha] = true
}
return commitShaMap
}
func (gui *Gui) addCommitToCherryPickedCommits(index int) {
// not super happy with modifying the state of the Commits array here
// but the alternative would be very tricky
gui.State.Commits[index].Copied = true
commitShaMap := gui.cherryPickedCommitShaMap()
commitShaMap[gui.State.Commits[index].Sha] = true
newCommits := []*commands.Commit{}
for _, commit := range gui.State.Commits {
if commit.Copied {
if commitShaMap[commit.Sha] {
// duplicating just the things we need to put in the rebase TODO list
newCommits = append(newCommits, &commands.Commit{Name: commit.Name, Sha: commit.Sha})
}
@@ -368,13 +444,17 @@ func (gui *Gui) addCommitToCherryPickedCommits(index int) {
}
func (gui *Gui) handleCopyCommitRange(g *gocui.Gui, v *gocui.View) error {
// whenever I add a commit, I need to make sure I retain its order
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
commitShaMap := gui.cherryPickedCommitShaMap()
// find the last commit that is copied that's above our position
// if there are none, startIndex = 0
startIndex := 0
for index, commit := range gui.State.Commits[0:gui.State.Panels.Commits.SelectedLine] {
if commit.Copied {
if commitShaMap[commit.Sha] {
startIndex = index
}
}
@@ -385,11 +465,15 @@ func (gui *Gui) handleCopyCommitRange(g *gocui.Gui, v *gocui.View) error {
gui.addCommitToCherryPickedCommits(index)
}
return gui.refreshCommits(gui.g)
return gui.renderBranchCommitsWithSelection()
}
// HandlePasteCommits begins a cherry-pick rebase with the commits the user has copied
func (gui *Gui) HandlePasteCommits(g *gocui.Gui, v *gocui.View) error {
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
return gui.createConfirmationPanel(g, v, true, gui.Tr.SLocalize("CherryPick"), gui.Tr.SLocalize("SureCherryPick"), func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.SLocalize("CherryPickingStatus"), func() error {
err := gui.GitCommand.CherryPickCommits(gui.State.CherryPickedCommits)
@@ -406,54 +490,6 @@ func (gui *Gui) handleSwitchToCommitFilesPanel(g *gocui.Gui, v *gocui.View) erro
return gui.switchFocus(g, gui.getCommitsView(), gui.getCommitFilesView())
}
func (gui *Gui) handleToggleDiffCommit(g *gocui.Gui, v *gocui.View) error {
selectLimit := 2
// get selected commit
commit := gui.getSelectedCommit(g)
if commit == nil {
return gui.newStringTask("main", gui.Tr.SLocalize("NoCommitsThisBranch"))
}
// if already selected commit delete
if idx, has := gui.hasCommit(gui.State.DiffEntries, commit.Sha); has {
gui.State.DiffEntries = gui.unchooseCommit(gui.State.DiffEntries, idx)
} else {
if len(gui.State.DiffEntries) == selectLimit {
gui.State.DiffEntries = gui.unchooseCommit(gui.State.DiffEntries, 0)
}
gui.State.DiffEntries = append(gui.State.DiffEntries, commit)
}
gui.setDiffMode()
// if selected two commits, display diff between
if len(gui.State.DiffEntries) == selectLimit {
commitText, err := gui.GitCommand.DiffCommits(gui.State.DiffEntries[0].Sha, gui.State.DiffEntries[1].Sha)
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.newStringTask("main", commitText)
}
return nil
}
func (gui *Gui) setDiffMode() {
v := gui.getCommitsView()
if len(gui.State.DiffEntries) != 0 {
gui.State.Panels.Commits.SpecificDiffMode = true
v.Title = gui.Tr.SLocalize("CommitsDiffTitle")
} else {
gui.State.Panels.Commits.SpecificDiffMode = false
v.Title = gui.Tr.SLocalize("CommitsTitle")
}
_ = gui.refreshCommits(gui.g)
}
func (gui *Gui) hasCommit(commits []*commands.Commit, target string) (int, bool) {
for idx, commit := range commits {
if commit.Sha == target {
@@ -468,7 +504,11 @@ func (gui *Gui) unchooseCommit(commits []*commands.Commit, i int) []*commands.Co
}
func (gui *Gui) handleCreateFixupCommit(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedCommit(g)
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
commit := gui.getSelectedCommit()
if commit == nil {
return nil
}
@@ -480,15 +520,19 @@ func (gui *Gui) handleCreateFixupCommit(g *gocui.Gui, v *gocui.View) error {
},
), func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.CreateFixupCommit(commit.Sha); err != nil {
return gui.createErrorPanel(g, err.Error())
return gui.surfaceError(err)
}
return gui.refreshSidePanels(gui.g)
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}, nil)
}
func (gui *Gui) handleSquashAllAboveFixupCommits(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedCommit(g)
if ok, err := gui.validateNotInFilterMode(); err != nil || !ok {
return err
}
commit := gui.getSelectedCommit()
if commit == nil {
return nil
}
@@ -510,7 +554,7 @@ func (gui *Gui) handleTagCommit(g *gocui.Gui, v *gocui.View) error {
// TODO: bring up menu asking if you want to make a lightweight or annotated tag
// if annotated, switch to a subprocess to create the message
commit := gui.getSelectedCommit(g)
commit := gui.getSelectedCommit()
if commit == nil {
return nil
}
@@ -521,26 +565,20 @@ func (gui *Gui) handleTagCommit(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handleCreateLightweightTag(commitSha string) error {
return gui.createPromptPanel(gui.g, gui.getCommitsView(), gui.Tr.SLocalize("TagNameTitle"), "", func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.CreateLightweightTag(v.Buffer(), commitSha); err != nil {
return gui.createErrorPanel(g, err.Error())
return gui.surfaceError(err)
}
if err := gui.refreshCommits(g); err != nil {
return gui.createErrorPanel(g, err.Error())
}
if err := gui.refreshTags(); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return gui.handleCommitSelect(g, v)
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{COMMITS, TAGS}})
})
}
func (gui *Gui) handleCheckoutCommit(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedCommit(g)
commit := gui.getSelectedCommit()
if commit == nil {
return nil
}
return gui.createConfirmationPanel(g, gui.getCommitsView(), true, gui.Tr.SLocalize("checkoutCommit"), gui.Tr.SLocalize("SureCheckoutThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
return gui.handleCheckoutRef(commit.Sha)
return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{})
}, nil)
}
@@ -548,7 +586,7 @@ 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)
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 {
@@ -610,9 +648,9 @@ func (gui *Gui) handlePrevCommitsTab(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleCreateCommitResetMenu(g *gocui.Gui, v *gocui.View) error {
commit := gui.getSelectedCommit(g)
commit := gui.getSelectedCommit()
if commit == nil {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NoCommitsThisBranch"))
return gui.createErrorPanel(gui.Tr.SLocalize("NoCommitsThisBranch"))
}
return gui.createResetMenu(commit.Sha)
@@ -635,10 +673,33 @@ func (gui *Gui) handleOpenSearchForCommitsPanel(g *gocui.Gui, v *gocui.View) err
// 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.refreshCommits(gui.g); err != nil {
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
}

View File

@@ -39,7 +39,8 @@ func (gui *Gui) closeConfirmationPrompt(g *gocui.Gui, returnFocusOnClose bool) e
panic(err)
}
}
g.DeleteKeybindings("confirmation")
g.DeleteKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone)
g.DeleteKeybinding("confirmation", gocui.KeyEsc, gocui.ModNone)
return g.DeleteView("confirmation")
}
@@ -61,6 +62,9 @@ func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, wrap bool, prompt s
width, height := g.Size()
panelWidth := 4 * width / 7
panelHeight := gui.getMessageHeight(wrap, prompt, panelWidth)
if panelHeight > height*3/4 {
panelHeight = height * 3 / 4
}
return width/2 - panelWidth/2,
height/2 - panelHeight/2 - panelHeight%2 - 1,
width/2 + panelWidth/2,
@@ -149,6 +153,7 @@ 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 {
return err
@@ -174,9 +179,17 @@ func (gui *Gui) createSpecificErrorPanel(message string, nextView *gocui.View, w
colorFunction := color.New(color.FgRed).SprintFunc()
coloredMessage := colorFunction(strings.TrimSpace(message))
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil {
return err
}
return gui.createConfirmationPanel(gui.g, nextView, true, gui.Tr.SLocalize("Error"), coloredMessage, nil, nil)
}
func (gui *Gui) createErrorPanel(g *gocui.Gui, message string) error {
return gui.createSpecificErrorPanel(message, g.CurrentView(), true)
func (gui *Gui) createErrorPanel(message string) error {
return gui.createSpecificErrorPanel(message, gui.g.CurrentView(), true)
}
func (gui *Gui) surfaceError(err error) error {
return gui.createErrorPanel(err.Error())
}

View File

@@ -36,19 +36,9 @@ func (gui *Gui) waitForPassUname(g *gocui.Gui, currentView *gocui.View, passOrUn
func (gui *Gui) handleSubmitCredential(g *gocui.Gui, v *gocui.View) error {
message := gui.trimmedContent(v)
gui.credentials <- message
err := gui.refreshFiles()
if err != nil {
return err
}
v.Clear()
err = v.SetCursor(0, 0)
if err != nil {
return err
}
_, err = g.SetViewOnBottom("credentials")
if err != nil {
return err
}
_ = v.SetCursor(0, 0)
_, _ = g.SetViewOnBottom("credentials")
nextView, err := gui.g.View("confirmation")
if err != nil {
nextView = gui.getFilesView()
@@ -57,7 +47,7 @@ func (gui *Gui) handleSubmitCredential(g *gocui.Gui, v *gocui.View) error {
if err != nil {
return err
}
return gui.refreshCommits(g)
return gui.refreshSidePanels(refreshOptions{})
}
func (gui *Gui) handleCloseCredentialsView(g *gocui.Gui, v *gocui.View) error {
@@ -100,6 +90,6 @@ func (gui *Gui) HandleCredentialsPopup(g *gocui.Gui, popupOpened bool, cmdErr er
_ = gui.createSpecificErrorPanel(errMessage, gui.getFilesView(), false)
} else {
_ = gui.closeConfirmationPrompt(g, true)
_ = gui.refreshSidePanels(g)
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}
}

181
pkg/gui/diffing.go Normal file
View File

@@ -0,0 +1,181 @@
package gui
import (
"fmt"
"strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
)
func (gui *Gui) inDiffMode() bool {
return gui.State.Diff.Ref != ""
}
func (gui *Gui) exitDiffMode() error {
gui.State.Diff = DiffState{}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}
func (gui *Gui) renderDiff() error {
gui.getMainView().Title = "Diff"
gui.State.SplitMainPanel = false
filterArg := ""
if gui.inFilterMode() {
filterArg = fmt.Sprintf(" -- %s", gui.State.FilterPath)
}
cmd := gui.OSCommand.ExecutableFromString(
fmt.Sprintf("git diff --color %s %s", gui.diffStr(), filterArg),
)
if err := gui.newPtyTask("main", cmd); err != nil {
gui.Log.Error(err)
}
return nil
}
// currentDiffTerminals returns the current diff terminals of the currently selected item.
// in the case of a branch it returns both the branch and it's upstream name,
// which becomes an option when you bring up the diff menu, but when you're just
// flicking through branches it will be using the local branch name.
func (gui *Gui) currentDiffTerminals() []string {
currentView := gui.g.CurrentView()
if currentView == nil {
return nil
}
names := []string{}
switch currentView.Name() {
case "files":
// not supporting files for now
// file, err := gui.getSelectedFile()
// if err == nil {
// names = append(names, file.Name)
// }
case "commitFiles":
// not supporting commit files for now
// file := gui.getSelectedCommitFile()
// if file != nil {
// names = append(names, file.Name)
// }
case "commits":
var commit *commands.Commit
switch gui.getCommitsView().Context {
case "reflog-commits":
commit = gui.getSelectedReflogCommit()
case "branch-commits":
commit = gui.getSelectedCommit()
}
if commit != nil {
names = append(names, commit.Sha)
}
case "stash":
entry := gui.getSelectedStashEntry()
if entry != nil {
names = append(names, entry.RefName())
}
case "branches":
switch gui.getBranchesView().Context {
case "local-branches":
branch := gui.getSelectedBranch()
if branch != nil {
names = append(names, branch.Name)
if branch.UpstreamName != "" {
names = append(names, branch.UpstreamName)
}
}
case "remotes":
remote := gui.getSelectedRemote()
if remote != nil {
names = append(names, remote.Name)
}
case "remote-branches":
remoteBranch := gui.getSelectedRemoteBranch()
if remoteBranch != nil {
names = append(names, remoteBranch.FullName())
}
case "tags":
tag := gui.getSelectedTag()
if tag != nil {
names = append(names, tag.Name)
}
}
}
return names
}
func (gui *Gui) currentDiffTerminal() string {
names := gui.currentDiffTerminals()
if len(names) == 0 {
return ""
}
return names[0]
}
func (gui *Gui) diffStr() string {
output := gui.State.Diff.Ref
right := gui.currentDiffTerminal()
if right != "" {
output += " " + right
}
if gui.State.Diff.Reverse {
output += " -R"
}
return output
}
func (gui *Gui) handleCreateDiffingMenuPanel(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
names := gui.currentDiffTerminals()
menuItems := []*menuItem{}
for _, name := range names {
name := name
menuItems = append(menuItems, []*menuItem{
{
displayString: fmt.Sprintf("%s %s", gui.Tr.SLocalize("diff"), name),
onPress: func() error {
gui.State.Diff.Ref = name
// can scope this down based on current view but too lazy right now
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
},
},
}...)
}
menuItems = append(menuItems, []*menuItem{
{
displayString: gui.Tr.SLocalize("enterRefToDiff"),
onPress: func() error {
return gui.createPromptPanel(gui.g, v, gui.Tr.SLocalize("enteRefName"), "", func(g *gocui.Gui, promptView *gocui.View) error {
gui.State.Diff.Ref = strings.TrimSpace(promptView.Buffer())
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
})
},
},
}...)
if gui.inDiffMode() {
menuItems = append(menuItems, []*menuItem{
{
displayString: gui.Tr.SLocalize("swapDiff"),
onPress: func() error {
gui.State.Diff.Reverse = !gui.State.Diff.Reverse
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
},
},
{
displayString: gui.Tr.SLocalize("exitDiffMode"),
onPress: func() error {
gui.State.Diff = DiffState{}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
},
},
}...)
}
return gui.createMenu(gui.Tr.SLocalize("DiffingMenuTitle"), menuItems, createMenuOptions{showCancel: true})
}

View File

@@ -5,7 +5,7 @@ import (
)
func (gui *Gui) handleCreateDiscardMenu(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
file, err := gui.getSelectedFile()
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
@@ -18,9 +18,9 @@ func (gui *Gui) handleCreateDiscardMenu(g *gocui.Gui, v *gocui.View) error {
displayString: gui.Tr.SLocalize("discardAllChanges"),
onPress: func() error {
if err := gui.GitCommand.DiscardAllFileChanges(file); err != nil {
return err
return gui.surfaceError(err)
}
return gui.refreshFiles()
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
},
},
}
@@ -30,10 +30,10 @@ func (gui *Gui) handleCreateDiscardMenu(g *gocui.Gui, v *gocui.View) error {
displayString: gui.Tr.SLocalize("discardUnstagedChanges"),
onPress: func() error {
if err := gui.GitCommand.DiscardUnstagedFileChanges(file); err != nil {
return err
return gui.surfaceError(err)
}
return gui.refreshFiles()
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
},
})
}

View File

@@ -132,12 +132,7 @@ func (gui *Gui) watchFilesForChanges() {
}
// only refresh if we're not already
if !gui.State.IsRefreshingFiles {
if err := gui.refreshFiles(); err != nil {
err = gui.createErrorPanel(gui.g, err.Error())
if err != nil {
gui.Log.Error(err)
}
}
gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
}
// watch for errors

View File

@@ -17,7 +17,7 @@ import (
// list panel functions
func (gui *Gui) getSelectedFile(g *gocui.Gui) (*commands.File, error) {
func (gui *Gui) getSelectedFile() (*commands.File, error) {
selectedLine := gui.State.Panels.Files.SelectedLine
if selectedLine == -1 {
return &commands.File{}, gui.Errors.ErrNoFiles
@@ -27,7 +27,13 @@ func (gui *Gui) getSelectedFile(g *gocui.Gui) (*commands.File, error) {
}
func (gui *Gui) selectFile(alreadySelected bool) error {
file, err := gui.getSelectedFile(gui.g)
gui.getFilesView().FocusPoint(0, gui.State.Panels.Files.SelectedLine)
if gui.inDiffMode() {
return gui.renderDiff()
}
file, err := gui.getSelectedFile()
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
@@ -37,14 +43,6 @@ func (gui *Gui) selectFile(alreadySelected bool) error {
return gui.newStringTask("main", gui.Tr.SLocalize("NoChangedFiles"))
}
gui.getFilesView().FocusPoint(0, gui.State.Panels.Files.SelectedLine)
if file.HasInlineMergeConflicts {
gui.getMainView().Title = gui.Tr.SLocalize("MergeConflictsTitle")
gui.State.SplitMainPanel = false
return gui.refreshMergePanel()
}
if !alreadySelected {
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
@@ -54,6 +52,12 @@ func (gui *Gui) selectFile(alreadySelected bool) error {
}
}
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")
@@ -89,7 +93,7 @@ func (gui *Gui) refreshFiles() error {
gui.State.RefreshingFilesMutex.Unlock()
}()
selectedFile, _ := gui.getSelectedFile(gui.g)
selectedFile, _ := gui.getSelectedFile()
filesView := gui.getFilesView()
if filesView == nil {
@@ -101,11 +105,11 @@ func (gui *Gui) refreshFiles() error {
}
gui.g.Update(func(g *gocui.Gui) error {
displayStrings := presentation.GetFileListDisplayStrings(gui.State.Files)
displayStrings := presentation.GetFileListDisplayStrings(gui.State.Files, gui.State.Diff.Ref)
gui.renderDisplayStrings(filesView, displayStrings)
if g.CurrentView() == filesView || (g.CurrentView() == gui.getMainView() && g.CurrentView().Context == "merging") {
newSelectedFile, _ := gui.getSelectedFile(gui.g)
newSelectedFile, _ := gui.getSelectedFile()
alreadySelected := newSelectedFile.Name == selectedFile.Name
return gui.selectFile(alreadySelected)
}
@@ -130,7 +134,7 @@ func (gui *Gui) stagedFiles() []*commands.File {
func (gui *Gui) trackedFiles() []*commands.File {
files := gui.State.Files
result := make([]*commands.File, 0)
result := make([]*commands.File, 0, len(files))
for _, file := range files {
if file.Tracked {
result = append(result, file)
@@ -140,7 +144,7 @@ func (gui *Gui) trackedFiles() []*commands.File {
}
func (gui *Gui) stageSelectedFile(g *gocui.Gui) error {
file, err := gui.getSelectedFile(g)
file, err := gui.getSelectedFile()
if err != nil {
return err
}
@@ -152,7 +156,7 @@ func (gui *Gui) handleEnterFile(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) enterFile(forceSecondaryFocused bool, selectedLineIdx int) error {
file, err := gui.getSelectedFile(gui.g)
file, err := gui.getSelectedFile()
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
@@ -163,7 +167,7 @@ func (gui *Gui) enterFile(forceSecondaryFocused bool, selectedLineIdx int) error
return gui.handleSwitchToMerge(gui.g, gui.getFilesView())
}
if file.HasMergeConflicts {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("FileStagingRequirements"))
return gui.createErrorPanel(gui.Tr.SLocalize("FileStagingRequirements"))
}
gui.changeMainViewsContext("staging")
if err := gui.switchFocus(gui.g, gui.getFilesView(), gui.getMainView()); err != nil {
@@ -173,7 +177,7 @@ func (gui *Gui) enterFile(forceSecondaryFocused bool, selectedLineIdx int) error
}
func (gui *Gui) handleFilePress(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
file, err := gui.getSelectedFile()
if err != nil {
if err == gui.Errors.ErrNoFiles {
return nil
@@ -191,10 +195,10 @@ func (gui *Gui) handleFilePress(g *gocui.Gui, v *gocui.View) error {
err = gui.GitCommand.UnStageFile(file.Name, file.Tracked)
}
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
if err := gui.refreshFiles(); err != nil {
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
return err
}
@@ -226,10 +230,10 @@ func (gui *Gui) handleStageAll(g *gocui.Gui, v *gocui.View) error {
err = gui.GitCommand.StageAll()
}
if err != nil {
_ = gui.createErrorPanel(g, err.Error())
_ = gui.surfaceError(err)
}
if err := gui.refreshFiles(); err != nil {
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
return err
}
@@ -237,9 +241,9 @@ func (gui *Gui) handleStageAll(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(gui.g)
file, err := gui.getSelectedFile()
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
if file.Tracked {
@@ -252,21 +256,21 @@ func (gui *Gui) handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.RemoveTrackedFiles(file.Name); err != nil {
return err
}
return gui.refreshFiles()
return gui.refreshSidePanels(refreshOptions{scope: []int{FILES}})
}, nil)
}
if err := gui.GitCommand.Ignore(file.Name); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
return gui.refreshFiles()
return gui.refreshSidePanels(refreshOptions{scope: []int{FILES}})
}
func (gui *Gui) handleWIPCommitPress(g *gocui.Gui, filesView *gocui.View) error {
skipHookPreifx := gui.Config.GetUserConfig().GetString("git.skipHookPrefix")
if skipHookPreifx == "" {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("SkipHookPrefixNotConfigured"))
return gui.createErrorPanel(gui.Tr.SLocalize("SkipHookPrefixNotConfigured"))
}
gui.renderString(g, "commitMessage", skipHookPreifx)
@@ -278,8 +282,8 @@ func (gui *Gui) handleWIPCommitPress(g *gocui.Gui, filesView *gocui.View) error
}
func (gui *Gui) handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
if len(gui.stagedFiles()) == 0 && gui.State.WorkingTreeState == "normal" {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit"))
if len(gui.stagedFiles()) == 0 && gui.GitCommand.WorkingTreeState() == "normal" {
return gui.createErrorPanel(gui.Tr.SLocalize("NoStagedFilesToCommit"))
}
commitMessageView := gui.getCommitMessageView()
g.Update(func(g *gocui.Gui) error {
@@ -298,11 +302,11 @@ func (gui *Gui) handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
}
func (gui *Gui) handleAmendCommitPress(g *gocui.Gui, filesView *gocui.View) error {
if len(gui.stagedFiles()) == 0 && gui.State.WorkingTreeState == "normal" {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit"))
if len(gui.stagedFiles()) == 0 && gui.GitCommand.WorkingTreeState() == "normal" {
return gui.createErrorPanel(gui.Tr.SLocalize("NoStagedFilesToCommit"))
}
if len(gui.State.Commits) == 0 {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoCommitToAmend"))
return gui.createErrorPanel(gui.Tr.SLocalize("NoCommitToAmend"))
}
title := strings.Title(gui.Tr.SLocalize("AmendLastCommit"))
@@ -317,15 +321,15 @@ func (gui *Gui) handleAmendCommitPress(g *gocui.Gui, filesView *gocui.View) erro
return nil
}
return gui.refreshSidePanels(g)
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
}, nil)
}
// handleCommitEditorPress - handle when the user wants to commit changes via
// their editor rather than via the popup panel
func (gui *Gui) handleCommitEditorPress(g *gocui.Gui, filesView *gocui.View) error {
if len(gui.stagedFiles()) == 0 && gui.State.WorkingTreeState == "normal" {
return gui.createErrorPanel(g, gui.Tr.SLocalize("NoStagedFilesToCommit"))
if len(gui.stagedFiles()) == 0 && gui.GitCommand.WorkingTreeState() == "normal" {
return gui.createErrorPanel(gui.Tr.SLocalize("NoStagedFilesToCommit"))
}
gui.PrepareSubProcess(g, "git", "commit")
return nil
@@ -345,24 +349,24 @@ func (gui *Gui) editFile(filename string) error {
}
func (gui *Gui) handleFileEdit(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
file, err := gui.getSelectedFile()
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
return gui.editFile(file.Name)
}
func (gui *Gui) handleFileOpen(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
file, err := gui.getSelectedFile()
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
return gui.openFile(file.Name)
}
func (gui *Gui) handleRefreshFiles(g *gocui.Gui, v *gocui.View) error {
return gui.refreshFiles()
return gui.refreshSidePanels(refreshOptions{scope: []int{FILES}})
}
func (gui *Gui) refreshStateFiles() error {
@@ -375,11 +379,11 @@ func (gui *Gui) refreshStateFiles() error {
}
gui.refreshSelectedLine(&gui.State.Panels.Files.SelectedLine, len(gui.State.Files))
return gui.updateWorkTreeState()
return nil
}
func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) {
item, err := gui.getSelectedFile(g)
item, err := gui.getSelectedFile()
if err != nil {
if err != gui.Errors.ErrNoFiles {
return "", err
@@ -404,7 +408,7 @@ func (gui *Gui) handlePullFiles(g *gocui.Gui, v *gocui.View) error {
// see if we have this branch in our config with an upstream
conf, err := gui.GitCommand.Repo.Config()
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
for branchName, branch := range conf.Branches {
if branchName == currentBranch.Name {
@@ -419,7 +423,7 @@ func (gui *Gui) handlePullFiles(g *gocui.Gui, v *gocui.View) error {
if strings.Contains(errorMessage, "does not exist") {
errorMessage = fmt.Sprintf("upstream branch %s not found.\nIf you expect it to exist, you should fetch (with 'f').\nOtherwise, you should push (with 'shift+P')", upstream)
}
return gui.createErrorPanel(gui.g, errorMessage)
return gui.createErrorPanel(errorMessage)
}
return gui.pullFiles(v, "")
})
@@ -469,7 +473,7 @@ func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error {
// see if we have this branch in our config with an upstream
conf, err := gui.GitCommand.Repo.Config()
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
for branchName, branch := range conf.Branches {
if branchName == currentBranch.Name {
@@ -489,15 +493,15 @@ func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error {
file, err := gui.getSelectedFile(g)
file, err := gui.getSelectedFile()
if err != nil {
if err != gui.Errors.ErrNoFiles {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
return nil
}
if !file.HasInlineMergeConflicts {
return gui.createErrorPanel(g, gui.Tr.SLocalize("FileNoMergeCons"))
return gui.createErrorPanel(gui.Tr.SLocalize("FileNoMergeCons"))
}
gui.changeMainViewsContext("merging")
if err := gui.switchFocus(g, v, gui.getMainView()); err != nil {
@@ -508,7 +512,7 @@ func (gui *Gui) handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) openFile(filename string) error {
if err := gui.OSCommand.OpenFile(filename); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
return nil
}

21
pkg/gui/filtering.go Normal file
View File

@@ -0,0 +1,21 @@
package gui
import "github.com/jesseduffield/gocui"
func (gui *Gui) inFilterMode() bool {
return gui.State.FilterPath != ""
}
func (gui *Gui) validateNotInFilterMode() (bool, error) {
if gui.inFilterMode() {
return false, gui.createConfirmationPanel(gui.g, gui.g.CurrentView(), true, gui.Tr.SLocalize("MustExitFilterModeTitle"), gui.Tr.SLocalize("MustExitFilterModePrompt"), func(*gocui.Gui, *gocui.View) error {
return gui.exitFilterMode()
}, nil)
}
return true, nil
}
func (gui *Gui) exitFilterMode() error {
gui.State.FilterPath = ""
return gui.Errors.ErrRestart
}

View File

@@ -0,0 +1,62 @@
package gui
import (
"fmt"
"strings"
"github.com/jesseduffield/gocui"
)
func (gui *Gui) handleCreateFilteringMenuPanel(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() {
return nil
}
fileName := ""
switch v.Name() {
case "files":
file, err := gui.getSelectedFile()
if err == nil {
fileName = file.Name
}
case "commitFiles":
file := gui.getSelectedCommitFile()
if file != nil {
fileName = file.Name
}
}
menuItems := []*menuItem{}
if fileName != "" {
menuItems = append(menuItems, &menuItem{
displayString: fmt.Sprintf("%s '%s'", gui.Tr.SLocalize("filterBy"), fileName),
onPress: func() error {
gui.State.FilterPath = fileName
return gui.Errors.ErrRestart
},
})
}
menuItems = append(menuItems, &menuItem{
displayString: gui.Tr.SLocalize("filterPathOption"),
onPress: func() error {
return gui.createPromptPanel(gui.g, v, gui.Tr.SLocalize("enterFileName"), "", func(g *gocui.Gui, promptView *gocui.View) error {
gui.State.FilterPath = strings.TrimSpace(promptView.Buffer())
return gui.Errors.ErrRestart
})
},
})
if gui.inFilterMode() {
menuItems = append(menuItems, &menuItem{
displayString: gui.Tr.SLocalize("exitFilterMode"),
onPress: func() error {
gui.State.FilterPath = ""
return gui.Errors.ErrRestart
},
})
}
return gui.createMenu(gui.Tr.SLocalize("FilteringMenuTitle"), menuItems, createMenuOptions{showCancel: true})
}

View File

@@ -28,7 +28,7 @@ func (gui *Gui) gitFlowFinishBranch(gitFlowConfig string, branchName string) err
}
if branchType == "" {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NotAGitFlowBranch"))
return gui.createErrorPanel(gui.Tr.SLocalize("NotAGitFlowBranch"))
}
subProcess := gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "finish", suffix)
@@ -45,7 +45,7 @@ func (gui *Gui) handleCreateGitFlowMenu(g *gocui.Gui, v *gocui.View) error {
// get config
gitFlowConfig, err := gui.OSCommand.RunCommandWithOutput("git config --local --get-regexp gitflow")
if err != nil {
return gui.createErrorPanel(gui.g, "You need to install git-flow and enable it in this repo to use git-flow features")
return gui.createErrorPanel("You need to install git-flow and enable it in this repo to use git-flow features")
}
startHandler := func(branchType string) func() error {

180
pkg/gui/global_handlers.go Normal file
View File

@@ -0,0 +1,180 @@
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
}

File diff suppressed because it is too large Load Diff

View File

@@ -313,6 +313,20 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Modifier: gocui.ModNone,
Handler: gui.handleCreateOptionsMenu,
},
{
ViewName: "",
Key: gui.getKey("universal.undo"),
Modifier: gocui.ModNone,
Handler: gui.reflogUndo,
Description: gui.Tr.SLocalize("undoReflog"),
},
{
ViewName: "",
Key: gui.getKey("universal.redo"),
Modifier: gocui.ModNone,
Handler: gui.reflogRedo,
Description: gui.Tr.SLocalize("redoReflog"),
},
{
ViewName: "status",
Key: gui.getKey("universal.edit"),
@@ -618,16 +632,18 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Description: gui.Tr.SLocalize("viewResetOptions"),
},
{
ViewName: "branches",
Key: gui.getKey("universal.nextTab"),
Modifier: gocui.ModNone,
Handler: gui.handleNextBranchesTab,
ViewName: "branches",
Key: gui.getKey("universal.nextTab"),
Modifier: gocui.ModNone,
Handler: gui.handleNextBranchesTab,
Description: gui.Tr.SLocalize("nextTab"),
},
{
ViewName: "branches",
Key: gui.getKey("universal.prevTab"),
Modifier: gocui.ModNone,
Handler: gui.handlePrevBranchesTab,
ViewName: "branches",
Key: gui.getKey("universal.prevTab"),
Modifier: gocui.ModNone,
Handler: gui.handlePrevBranchesTab,
Description: gui.Tr.SLocalize("prevTab"),
},
{
ViewName: "branches",
@@ -654,23 +670,18 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Description: gui.Tr.SLocalize("fetchRemote"),
},
{
ViewName: "commits",
Key: gui.getKey("universal.nextTab"),
Modifier: gocui.ModNone,
Handler: gui.handleNextCommitsTab,
},
{
ViewName: "commits",
Key: gui.getKey("universal.prevTab"),
Modifier: gocui.ModNone,
Handler: gui.handlePrevCommitsTab,
ViewName: "commits",
Key: gui.getKey("universal.nextTab"),
Modifier: gocui.ModNone,
Handler: gui.handleNextCommitsTab,
Description: gui.Tr.SLocalize("nextTab"),
},
{
ViewName: "commits",
Key: gui.getKey("universal.startSearch"),
Key: gui.getKey("universal.prevTab"),
Modifier: gocui.ModNone,
Handler: gui.handleOpenSearchForCommitsPanel,
Description: gui.Tr.SLocalize("startSearch"),
Handler: gui.handlePrevCommitsTab,
Description: gui.Tr.SLocalize("prevTab"),
},
{
ViewName: "commits",
@@ -824,14 +835,6 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleCheckoutCommit,
Description: gui.Tr.SLocalize("checkoutCommit"),
},
{
ViewName: "commits",
Contexts: []string{"branch-commits"},
Key: gui.getKey("commits.toggleDiffCommit"),
Modifier: gocui.ModNone,
Handler: gui.handleToggleDiffCommit,
Description: gui.Tr.SLocalize("CommitsDiff"),
},
{
ViewName: "commits",
Contexts: []string{"branch-commits"},
@@ -840,6 +843,14 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleTagCommit,
Description: gui.Tr.SLocalize("tagCommit"),
},
{
ViewName: "commits",
Contexts: []string{"branch-commits"},
Key: gui.getKey("commits.resetCherryPick"),
Modifier: gocui.ModNone,
Handler: gui.handleResetCherryPick,
Description: gui.Tr.SLocalize("resetCherryPick"),
},
{
ViewName: "commits",
Contexts: []string{"reflog-commits"},
@@ -919,7 +930,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
ViewName: "information",
Key: gocui.MouseLeft,
Modifier: gocui.ModNone,
Handler: gui.handleDonate,
Handler: gui.handleInfoClick,
},
{
ViewName: "commitFiles",
@@ -963,6 +974,20 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleEnterCommitFile,
Description: gui.Tr.SLocalize("enterFile"),
},
{
ViewName: "",
Key: gui.getKey("universal.filteringMenu"),
Modifier: gocui.ModNone,
Handler: gui.handleCreateFilteringMenuPanel,
Description: gui.Tr.SLocalize("openScopingMenu"),
},
{
ViewName: "",
Key: gui.getKey("universal.diffingMenu"),
Modifier: gocui.ModNone,
Handler: gui.handleCreateDiffingMenuPanel,
Description: gui.Tr.SLocalize("openDiffingMenu"),
},
{
ViewName: "secondary",
Key: gocui.MouseWheelUp,
@@ -1330,10 +1355,10 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
{
ViewName: "main",
Contexts: []string{"merging"},
Key: gui.getKey("main.undo"),
Key: gui.getKey("universal.undo"),
Modifier: gocui.ModNone,
Handler: gui.handlePopFileSnapshot,
Description: gui.Tr.SLocalize("Undo"),
Description: gui.Tr.SLocalize("undo"),
},
{
ViewName: "branches",
@@ -1436,6 +1461,30 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Modifier: gocui.ModNone,
Handler: gui.handleSearchEscape,
},
{
ViewName: "confirmation",
Key: gui.getKey("universal.prevItem"),
Modifier: gocui.ModNone,
Handler: gui.scrollUpConfirmationPanel,
},
{
ViewName: "confirmation",
Key: gui.getKey("universal.nextItem"),
Modifier: gocui.ModNone,
Handler: gui.scrollDownConfirmationPanel,
},
{
ViewName: "confirmation",
Key: gui.getKey("universal.prevItem-alt"),
Modifier: gocui.ModNone,
Handler: gui.scrollUpConfirmationPanel,
},
{
ViewName: "confirmation",
Key: gui.getKey("universal.nextItem-alt"),
Modifier: gocui.ModNone,
Handler: gui.scrollDownConfirmationPanel,
},
}
for _, viewName := range []string{"status", "branches", "files", "commits", "commitFiles", "stash", "menu"} {
@@ -1460,14 +1509,39 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
{ViewName: listView.viewName, Contexts: []string{listView.context}, Key: gocui.MouseWheelUp, Modifier: gocui.ModNone, Handler: listView.handlePrevLine},
{ViewName: listView.viewName, Contexts: []string{listView.context}, Key: gui.getKey("universal.nextItem-alt"), Modifier: gocui.ModNone, Handler: listView.handleNextLine},
{ViewName: listView.viewName, Contexts: []string{listView.context}, Key: gui.getKey("universal.nextItem"), Modifier: gocui.ModNone, Handler: listView.handleNextLine},
{ViewName: listView.viewName, Contexts: []string{listView.context}, Key: gui.getKey("universal.prevPage"), Modifier: gocui.ModNone, Handler: listView.handlePrevPage, Description: gui.Tr.SLocalize("prevPage")},
{ViewName: listView.viewName, Contexts: []string{listView.context}, Key: gui.getKey("universal.nextPage"), Modifier: gocui.ModNone, Handler: listView.handleNextPage, Description: gui.Tr.SLocalize("nextPage")},
{ViewName: listView.viewName, Contexts: []string{listView.context}, Key: gui.getKey("universal.gotoTop"), Modifier: gocui.ModNone, Handler: listView.handleGotoTop, Description: gui.Tr.SLocalize("gotoTop")},
{ViewName: listView.viewName, Contexts: []string{listView.context}, Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, Handler: listView.handleNextLine},
{ViewName: listView.viewName, Contexts: []string{listView.context}, Key: gocui.MouseLeft, Modifier: gocui.ModNone, Handler: listView.handleClick},
}...)
// we need a specific keybinding for the commits panel beacuse it usually lazyloads commits
if listView.viewName != "commits" {
bindings = append(bindings, &Binding{ViewName: listView.viewName, Contexts: []string{listView.context}, Key: gui.getKey("universal.startSearch"), Modifier: gocui.ModNone, Handler: gui.handleOpenSearch, Description: gui.Tr.SLocalize("startSearch")})
// the commits panel needs to lazyload things so it has a couple of its own handlers
openSearchHandler := gui.handleOpenSearch
gotoBottomHandler := listView.handleGotoBottom
if listView.viewName == "commits" {
openSearchHandler = gui.handleOpenSearchForCommitsPanel
gotoBottomHandler = gui.handleGotoBottomForCommitsPanel
}
bindings = append(bindings, []*Binding{
{
ViewName: listView.viewName,
Contexts: []string{listView.context},
Key: gui.getKey("universal.startSearch"),
Modifier: gocui.ModNone,
Handler: openSearchHandler,
Description: gui.Tr.SLocalize("startSearch"),
},
{
ViewName: listView.viewName,
Contexts: []string{listView.context},
Key: gui.getKey("universal.gotoBottom"),
Modifier: gocui.ModNone,
Handler: gotoBottomHandler,
Description: gui.Tr.SLocalize("gotoBottom"),
},
}...)
}
return bindings

520
pkg/gui/layout.go Normal file
View File

@@ -0,0 +1,520 @@
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
}

View File

@@ -42,6 +42,40 @@ func (lv *listView) handleLineChange(change int) error {
return lv.handleItemSelect(lv.gui.g, view)
}
func (lv *listView) handleNextPage(g *gocui.Gui, v *gocui.View) error {
view, err := lv.gui.g.View(lv.viewName)
if err != nil {
return nil
}
_, height := view.Size()
delta := height - 1
if delta == 0 {
delta = 1
}
return lv.handleLineChange(delta)
}
func (lv *listView) handleGotoTop(g *gocui.Gui, v *gocui.View) error {
return lv.handleLineChange(-lv.getItemsLength())
}
func (lv *listView) handleGotoBottom(g *gocui.Gui, v *gocui.View) error {
return lv.handleLineChange(lv.getItemsLength())
}
func (lv *listView) handlePrevPage(g *gocui.Gui, v *gocui.View) error {
view, err := lv.gui.g.View(lv.viewName)
if err != nil {
return nil
}
_, height := view.Size()
delta := height - 1
if delta == 0 {
delta = 1
}
return lv.handleLineChange(-delta)
}
func (lv *listView) handleClick(g *gocui.Gui, v *gocui.View) error {
if !lv.gui.isPopupPanel(lv.viewName) && lv.gui.popupPanelFocused() {
return nil
@@ -149,7 +183,7 @@ func (gui *Gui) getListViews() []*listView {
{
viewName: "commits",
context: "reflog-commits",
getItemsLength: func() int { return len(gui.State.ReflogCommits) },
getItemsLength: func() int { return len(gui.State.FilteredReflogCommits) },
getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.ReflogCommits.SelectedLine },
handleFocus: gui.handleReflogCommitSelect,
handleItemSelect: gui.handleReflogCommitSelect,

View File

@@ -104,7 +104,7 @@ func (gui *Gui) isIndexToDelete(i int, conflict commands.Conflict, pick string)
}
func (gui *Gui) resolveConflict(g *gocui.Gui, conflict commands.Conflict, pick string) error {
gitFile, err := gui.getSelectedFile(g)
gitFile, err := gui.getSelectedFile()
if err != nil {
return err
}
@@ -130,7 +130,7 @@ func (gui *Gui) resolveConflict(g *gocui.Gui, conflict commands.Conflict, pick s
}
func (gui *Gui) pushFileSnapshot(g *gocui.Gui) error {
gitFile, err := gui.getSelectedFile(g)
gitFile, err := gui.getSelectedFile()
if err != nil {
return err
}
@@ -147,7 +147,7 @@ func (gui *Gui) handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error {
return nil
}
prevContent := gui.State.Panels.Merging.EditHistory.Pop().(string)
gitFile, err := gui.getSelectedFile(g)
gitFile, err := gui.getSelectedFile()
if err != nil {
return err
}
@@ -223,8 +223,10 @@ func (gui *Gui) refreshMergePanel() error {
mainView := gui.getMainView()
mainView.Wrap = false
gui.setViewContent(gui.g, mainView, content)
gui.Log.Warn("scrolling to conflict")
if err := gui.newStringTask("main", content); err != nil {
return err
}
if err := gui.scrollToConflict(gui.g); err != nil {
return err
}
@@ -255,13 +257,13 @@ func (gui *Gui) renderMergeOptions() error {
fmt.Sprintf("%s %s", gui.getKeyDisplay("universal.prevBlock"), gui.getKeyDisplay("universal.nextBlock")): gui.Tr.SLocalize("navigateConflicts"),
gui.getKeyDisplay("universal.select"): gui.Tr.SLocalize("pickHunk"),
gui.getKeyDisplay("main.pickBothHunks"): gui.Tr.SLocalize("pickBothHunks"),
gui.getKeyDisplay("main.undo"): gui.Tr.SLocalize("undo"),
gui.getKeyDisplay("universal.undo"): gui.Tr.SLocalize("undo"),
})
}
func (gui *Gui) handleEscapeMerge(g *gocui.Gui, v *gocui.View) error {
gui.State.Panels.Merging.EditHistory = stack.New()
if err := gui.refreshFiles(); err != nil {
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
return err
}
// it's possible this method won't be called from the merging view so we need to
@@ -276,12 +278,12 @@ func (gui *Gui) handleCompleteMerge() error {
if err := gui.stageSelectedFile(gui.g); err != nil {
return err
}
if err := gui.refreshFiles(); err != nil {
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
return err
}
// if we got conflicts after unstashing, we don't want to call any git
// commands to continue rebasing/merging here
if gui.State.WorkingTreeState == "normal" {
if gui.GitCommand.WorkingTreeState() == "normal" {
return gui.handleEscapeMerge(gui.g, gui.getMainView())
}
// if there are no more files with merge conflicts, we should ask whether the user wants to continue

View File

@@ -16,7 +16,7 @@ func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int) error {
gui.getSecondaryView().Title = "Custom Patch"
// get diff from commit file that's currently selected
commitFile := gui.getSelectedCommitFile(gui.g)
commitFile := gui.getSelectedCommitFile()
if commitFile == nil {
gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
return nil
@@ -56,7 +56,7 @@ func (gui *Gui) handleToggleSelectionForPatch(g *gocui.Gui, v *gocui.View) error
}
// add range of lines to those set for the file
commitFile := gui.getSelectedCommitFile(gui.g)
commitFile := gui.getSelectedCommitFile()
if commitFile == nil {
gui.renderString(gui.g, "commitFiles", gui.Tr.SLocalize("NoCommiteFiles"))
return nil

View File

@@ -8,7 +8,7 @@ import (
func (gui *Gui) handleCreatePatchOptionsMenu(g *gocui.Gui, v *gocui.View) error {
if !gui.GitCommand.PatchManager.CommitSelected() {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NoPatchError"))
return gui.createErrorPanel(gui.Tr.SLocalize("NoPatchError"))
}
menuItems := []*menuItem{
@@ -20,13 +20,21 @@ func (gui *Gui) handleCreatePatchOptionsMenu(g *gocui.Gui, v *gocui.View) error
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(gui.g)
selectedCommit := gui.getSelectedCommit()
if selectedCommit != nil && gui.GitCommand.PatchManager.CommitSha != selectedCommit.Sha {
// adding this option to index 1
menuItems = append(
@@ -55,8 +63,11 @@ func (gui *Gui) getPatchCommitIndex() int {
}
func (gui *Gui) validateNormalWorkingTreeState() (bool, error) {
if gui.State.WorkingTreeState != "normal" {
return false, gui.createErrorPanel(gui.g, gui.Tr.SLocalize("CantPatchWhileRebasingError"))
if gui.GitCommand.WorkingTreeState() != "normal" {
return false, gui.createErrorPanel(gui.Tr.SLocalize("CantPatchWhileRebasingError"))
}
if gui.GitCommand.WorkingTreeState() != "normal" {
return false, gui.createErrorPanel(gui.Tr.SLocalize("CantPatchWhileRebasingError"))
}
return true, nil
}
@@ -109,11 +120,32 @@ func (gui *Gui) handlePullPatchIntoWorkingTree() error {
return err
}
return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error {
commitIndex := gui.getPatchCommitIndex()
err := gui.GitCommand.PullPatchIntoIndex(gui.State.Commits, commitIndex, gui.GitCommand.PatchManager)
return gui.handleGenericMergeCommandResult(err)
})
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 {

View File

@@ -10,26 +10,36 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
func GetBranchListDisplayStrings(branches []*commands.Branch, fullDescription bool) [][]string {
func GetBranchListDisplayStrings(branches []*commands.Branch, fullDescription bool, diffName string) [][]string {
lines := make([][]string, len(branches))
for i := range branches {
lines[i] = getBranchDisplayStrings(branches[i], fullDescription)
diffed := branches[i].Name == diffName
lines[i] = getBranchDisplayStrings(branches[i], fullDescription, diffed)
}
return lines
}
// getBranchDisplayStrings returns the display string of branch
func getBranchDisplayStrings(b *commands.Branch, fullDescription bool) []string {
displayName := utils.ColoredString(b.Name, GetBranchColor(b.Name))
func getBranchDisplayStrings(b *commands.Branch, fullDescription bool, diffed bool) []string {
displayName := b.Name
if b.DisplayName != "" {
displayName = b.DisplayName
}
nameColorAttr := GetBranchColor(b.Name)
if diffed {
nameColorAttr = theme.DiffTerminalColor
}
coloredName := utils.ColoredString(displayName, nameColorAttr)
if b.Pushables != "" && b.Pullables != "" && b.Pushables != "?" && b.Pullables != "?" {
trackColor := color.FgYellow
if b.Pushables == "0" && b.Pullables == "0" {
trackColor = color.FgGreen
}
track := utils.ColoredString(fmt.Sprintf("↑%s↓%s", b.Pushables, b.Pullables), trackColor)
displayName = fmt.Sprintf("%s %s", displayName, track)
coloredName = fmt.Sprintf("%s %s", coloredName, track)
}
recencyColor := color.FgCyan
@@ -38,10 +48,10 @@ func getBranchDisplayStrings(b *commands.Branch, fullDescription bool) []string
}
if fullDescription {
return []string{utils.ColoredString(b.Recency, recencyColor), displayName, utils.ColoredString(b.UpstreamName, color.FgYellow)}
return []string{utils.ColoredString(b.Recency, recencyColor), coloredName, utils.ColoredString(b.UpstreamName, color.FgYellow)}
}
return []string{utils.ColoredString(b.Recency, recencyColor), displayName}
return []string{utils.ColoredString(b.Recency, recencyColor), coloredName}
}
// GetBranchColor branch color

View File

@@ -6,21 +6,23 @@ import (
"github.com/jesseduffield/lazygit/pkg/theme"
)
func GetCommitFileListDisplayStrings(branches []*commands.CommitFile) [][]string {
lines := make([][]string, len(branches))
func GetCommitFileListDisplayStrings(commitFiles []*commands.CommitFile, diffName string) [][]string {
lines := make([][]string, len(commitFiles))
for i := range branches {
lines[i] = getCommitFileDisplayStrings(branches[i])
for i := range commitFiles {
diffed := commitFiles[i].Name == diffName
lines[i] = getCommitFileDisplayStrings(commitFiles[i], diffed)
}
return lines
}
// getCommitFileDisplayStrings returns the display string of branch
func getCommitFileDisplayStrings(f *commands.CommitFile) []string {
func getCommitFileDisplayStrings(f *commands.CommitFile, diffed bool) []string {
yellow := color.New(color.FgYellow)
green := color.New(color.FgGreen)
defaultColor := color.New(theme.DefaultTextColor)
diffTerminalColor := color.New(theme.DiffTerminalColor)
var colour *color.Color
switch f.Status {
@@ -31,5 +33,8 @@ func getCommitFileDisplayStrings(f *commands.CommitFile) []string {
case commands.PART:
colour = yellow
}
if diffed {
colour = diffTerminalColor
}
return []string{colour.Sprint(f.DisplayString)}
}

View File

@@ -9,10 +9,10 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
func GetCommitListDisplayStrings(commits []*commands.Commit, fullDescription bool) [][]string {
func GetCommitListDisplayStrings(commits []*commands.Commit, fullDescription bool, cherryPickedCommitShaMap map[string]bool, diffName string) [][]string {
lines := make([][]string, len(commits))
var displayFunc func(*commands.Commit) []string
var displayFunc func(*commands.Commit, map[string]bool, bool) []string
if fullDescription {
displayFunc = getFullDescriptionDisplayStringsForCommit
} else {
@@ -20,20 +20,21 @@ func GetCommitListDisplayStrings(commits []*commands.Commit, fullDescription boo
}
for i := range commits {
lines[i] = displayFunc(commits[i])
diffed := commits[i].Sha == diffName
lines[i] = displayFunc(commits[i], cherryPickedCommitShaMap, diffed)
}
return lines
}
func getFullDescriptionDisplayStringsForCommit(c *commands.Commit) []string {
func getFullDescriptionDisplayStringsForCommit(c *commands.Commit, cherryPickedCommitShaMap map[string]bool, diffed bool) []string {
red := color.New(color.FgRed)
yellow := color.New(color.FgYellow)
green := color.New(color.FgGreen)
blue := color.New(color.FgBlue)
cyan := color.New(color.FgCyan)
defaultColor := color.New(theme.DefaultTextColor)
magenta := color.New(color.FgMagenta)
diffedColor := color.New(theme.DiffTerminalColor)
// for some reason, setting the background to blue pads out the other commits
// horizontally. For the sake of accessibility I'm considering this a feature,
@@ -52,19 +53,18 @@ func getFullDescriptionDisplayStringsForCommit(c *commands.Commit) []string {
shaColor = blue
case "reflog":
shaColor = blue
case "selected":
shaColor = magenta
default:
shaColor = defaultColor
}
if c.Copied {
if diffed {
shaColor = diffedColor
} else if cherryPickedCommitShaMap[c.Sha] {
shaColor = copied
}
tagString := ""
truncatedDate := utils.TruncateWithEllipsis(c.Date, 15)
secondColumnString := blue.Sprint(truncatedDate)
secondColumnString := blue.Sprint(utils.UnixToDate(c.UnixTimestamp))
if c.Action != "" {
secondColumnString = cyan.Sprint(c.Action)
} else if c.ExtraInfo != "" {
@@ -74,17 +74,17 @@ func getFullDescriptionDisplayStringsForCommit(c *commands.Commit) []string {
truncatedAuthor := utils.TruncateWithEllipsis(c.Author, 17)
return []string{shaColor.Sprint(c.Sha[:8]), secondColumnString, yellow.Sprint(truncatedAuthor), tagString + defaultColor.Sprint(c.Name)}
return []string{shaColor.Sprint(c.ShortSha()), secondColumnString, yellow.Sprint(truncatedAuthor), tagString + defaultColor.Sprint(c.Name)}
}
func getDisplayStringsForCommit(c *commands.Commit) []string {
func getDisplayStringsForCommit(c *commands.Commit, cherryPickedCommitShaMap map[string]bool, diffed bool) []string {
red := color.New(color.FgRed)
yellow := color.New(color.FgYellow)
green := color.New(color.FgGreen)
blue := color.New(color.FgBlue)
cyan := color.New(color.FgCyan)
defaultColor := color.New(theme.DefaultTextColor)
magenta := color.New(color.FgMagenta)
diffedColor := color.New(theme.DiffTerminalColor)
// for some reason, setting the background to blue pads out the other commits
// horizontally. For the sake of accessibility I'm considering this a feature,
@@ -103,13 +103,13 @@ func getDisplayStringsForCommit(c *commands.Commit) []string {
shaColor = blue
case "reflog":
shaColor = blue
case "selected":
shaColor = magenta
default:
shaColor = defaultColor
}
if c.Copied {
if diffed {
shaColor = diffedColor
} else if cherryPickedCommitShaMap[c.Sha] {
shaColor = copied
}
@@ -122,5 +122,5 @@ func getDisplayStringsForCommit(c *commands.Commit) []string {
tagString = utils.ColoredStringDirect(strings.Join(c.Tags, " "), tagColor) + " "
}
return []string{shaColor.Sprint(c.Sha[:8]), actionString + tagString + defaultColor.Sprint(c.Name)}
return []string{shaColor.Sprint(c.ShortSha()), actionString + tagString + defaultColor.Sprint(c.Name)}
}

View File

@@ -3,34 +3,42 @@ package presentation
import (
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/theme"
)
func GetFileListDisplayStrings(files []*commands.File) [][]string {
func GetFileListDisplayStrings(files []*commands.File, diffName string) [][]string {
lines := make([][]string, len(files))
for i := range files {
lines[i] = getFileDisplayStrings(files[i])
diffed := files[i].Name == diffName
lines[i] = getFileDisplayStrings(files[i], diffed)
}
return lines
}
// getFileDisplayStrings returns the display string of branch
func getFileDisplayStrings(f *commands.File) []string {
func getFileDisplayStrings(f *commands.File, diffed bool) []string {
// potentially inefficient to be instantiating these color
// objects with each render
red := color.New(color.FgRed)
green := color.New(color.FgGreen)
diffColor := color.New(theme.DiffTerminalColor)
if !f.Tracked && !f.HasStagedChanges {
return []string{red.Sprint(f.DisplayString)}
}
output := green.Sprint(f.DisplayString[0:1])
output += red.Sprint(f.DisplayString[1:3])
if f.HasUnstagedChanges {
output += red.Sprint(f.Name)
var restColor *color.Color
if diffed {
restColor = diffColor
} else if f.HasUnstagedChanges {
restColor = red
} else {
output += green.Sprint(f.Name)
restColor = green
}
output += restColor.Sprint(f.Name)
return []string{output}
}

View File

@@ -0,0 +1,45 @@
package presentation
import (
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func GetReflogCommitListDisplayStrings(commits []*commands.Commit, fullDescription bool, diffName string) [][]string {
lines := make([][]string, len(commits))
var displayFunc func(*commands.Commit, bool) []string
if fullDescription {
displayFunc = getFullDescriptionDisplayStringsForReflogCommit
} else {
displayFunc = getDisplayStringsForReflogCommit
}
for i := range commits {
diffed := commits[i].Sha == diffName
lines[i] = displayFunc(commits[i], diffed)
}
return lines
}
func getFullDescriptionDisplayStringsForReflogCommit(c *commands.Commit, diffed bool) []string {
colorAttr := theme.DefaultTextColor
if diffed {
colorAttr = theme.DiffTerminalColor
}
return []string{
utils.ColoredString(c.ShortSha(), color.FgBlue),
utils.ColoredString(utils.UnixToDate(c.UnixTimestamp), color.FgMagenta),
utils.ColoredString(c.Name, colorAttr),
}
}
func getDisplayStringsForReflogCommit(c *commands.Commit, diffed bool) []string {
defaultColor := color.New(theme.DefaultTextColor)
return []string{utils.ColoredString(c.ShortSha(), color.FgBlue), defaultColor.Sprint(c.Name)}
}

View File

@@ -2,22 +2,29 @@ package presentation
import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func GetRemoteBranchListDisplayStrings(branches []*commands.RemoteBranch) [][]string {
func GetRemoteBranchListDisplayStrings(branches []*commands.RemoteBranch, diffName string) [][]string {
lines := make([][]string, len(branches))
for i := range branches {
lines[i] = getRemoteBranchDisplayStrings(branches[i])
diffed := branches[i].FullName() == diffName
lines[i] = getRemoteBranchDisplayStrings(branches[i], diffed)
}
return lines
}
// getRemoteBranchDisplayStrings returns the display string of branch
func getRemoteBranchDisplayStrings(b *commands.RemoteBranch) []string {
displayName := utils.ColoredString(b.Name, GetBranchColor(b.Name))
func getRemoteBranchDisplayStrings(b *commands.RemoteBranch, diffed bool) []string {
nameColorAttr := GetBranchColor(b.Name)
if diffed {
nameColorAttr = theme.DiffTerminalColor
}
displayName := utils.ColoredString(b.Name, nameColorAttr)
return []string{displayName}
}

View File

@@ -1,20 +1,33 @@
package presentation
import (
"fmt"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func GetRemoteListDisplayStrings(remotes []*commands.Remote) [][]string {
func GetRemoteListDisplayStrings(remotes []*commands.Remote, diffName string) [][]string {
lines := make([][]string, len(remotes))
for i := range remotes {
lines[i] = getRemoteDisplayStrings(remotes[i])
diffed := remotes[i].Name == diffName
lines[i] = getRemoteDisplayStrings(remotes[i], diffed)
}
return lines
}
// getRemoteDisplayStrings returns the display string of branch
func getRemoteDisplayStrings(r *commands.Remote) []string {
return []string{r.Name}
func getRemoteDisplayStrings(r *commands.Remote, diffed bool) []string {
branchCount := len(r.Branches)
nameColorAttr := theme.DefaultTextColor
if diffed {
nameColorAttr = theme.DiffTerminalColor
}
return []string{utils.ColoredString(r.Name, nameColorAttr), utils.ColoredString(fmt.Sprintf("%d branches", branchCount), color.FgBlue)}
}

View File

@@ -2,19 +2,26 @@ package presentation
import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func GetStashEntryListDisplayStrings(stashEntries []*commands.StashEntry) [][]string {
func GetStashEntryListDisplayStrings(stashEntries []*commands.StashEntry, diffName string) [][]string {
lines := make([][]string, len(stashEntries))
for i := range stashEntries {
lines[i] = getStashEntryDisplayStrings(stashEntries[i])
diffed := stashEntries[i].RefName() == diffName
lines[i] = getStashEntryDisplayStrings(stashEntries[i], diffed)
}
return lines
}
// getStashEntryDisplayStrings returns the display string of branch
func getStashEntryDisplayStrings(s *commands.StashEntry) []string {
return []string{s.DisplayString}
func getStashEntryDisplayStrings(s *commands.StashEntry, diffed bool) []string {
attr := theme.DefaultTextColor
if diffed {
attr = theme.DiffTerminalColor
}
return []string{utils.ColoredString(s.Name, attr)}
}

View File

@@ -2,19 +2,26 @@ package presentation
import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func GetTagListDisplayStrings(tags []*commands.Tag) [][]string {
func GetTagListDisplayStrings(tags []*commands.Tag, diffName string) [][]string {
lines := make([][]string, len(tags))
for i := range tags {
lines[i] = getTagDisplayStrings(tags[i])
diffed := tags[i].Name == diffName
lines[i] = getTagDisplayStrings(tags[i], diffed)
}
return lines
}
// getTagDisplayStrings returns the display string of branch
func getTagDisplayStrings(t *commands.Tag) []string {
return []string{t.Name}
func getTagDisplayStrings(t *commands.Tag, diffed bool) []string {
attr := theme.DefaultTextColor
if diffed {
attr = theme.DiffTerminalColor
}
return []string{utils.ColoredString(t.Name, attr)}
}

View File

@@ -5,7 +5,7 @@ package gui
import (
"os/exec"
"github.com/jesseduffield/pty"
"github.com/creack/pty"
)
func (gui *Gui) onResize() error {

View File

@@ -38,6 +38,12 @@ func (gui *Gui) quit(v *gocui.View) error {
if gui.State.Updating {
return gui.createUpdateQuitConfirmation(gui.g, v)
}
if gui.inDiffMode() {
return gui.exitDiffMode()
}
if gui.inFilterMode() {
return gui.exitFilterMode()
}
if gui.Config.GetUserConfig().GetBool("confirmOnQuit") {
return gui.createConfirmationPanel(gui.g, v, true, "", gui.Tr.SLocalize("ConfirmQuit"), func(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit

View File

@@ -10,7 +10,7 @@ import (
func (gui *Gui) handleCreateRebaseOptionsMenu(g *gocui.Gui, v *gocui.View) error {
options := []string{"continue", "abort"}
if gui.State.WorkingTreeState == "rebasing" {
if gui.GitCommand.WorkingTreeState() == "rebasing" {
options = append(options, "skip")
}
@@ -27,7 +27,7 @@ func (gui *Gui) handleCreateRebaseOptionsMenu(g *gocui.Gui, v *gocui.View) error
}
var title string
if gui.State.WorkingTreeState == "merging" {
if gui.GitCommand.WorkingTreeState() == "merging" {
title = gui.Tr.SLocalize("MergeOptionsTitle")
} else {
title = gui.Tr.SLocalize("RebaseOptionsTitle")
@@ -37,10 +37,10 @@ func (gui *Gui) handleCreateRebaseOptionsMenu(g *gocui.Gui, v *gocui.View) error
}
func (gui *Gui) genericMergeCommand(command string) error {
status := gui.State.WorkingTreeState
status := gui.GitCommand.WorkingTreeState()
if status != "merging" && status != "rebasing" {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NotMergingOrRebasing"))
return gui.createErrorPanel(gui.Tr.SLocalize("NotMergingOrRebasing"))
}
commandType := strings.Replace(status, "ing", "e", 1)
@@ -63,7 +63,7 @@ func (gui *Gui) genericMergeCommand(command string) error {
}
func (gui *Gui) handleGenericMergeCommandResult(result error) error {
if err := gui.refreshSidePanels(gui.g); err != nil {
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil {
return err
}
if result == nil {
@@ -74,6 +74,9 @@ func (gui *Gui) handleGenericMergeCommandResult(result error) error {
return gui.genericMergeCommand("skip")
} else if strings.Contains(result.Error(), "The previous cherry-pick is now empty") {
return gui.genericMergeCommand("continue")
} else if strings.Contains(result.Error(), "No rebase in progress?") {
// assume in this case that we're already done
return nil
} else if strings.Contains(result.Error(), "When you have resolved this problem") || strings.Contains(result.Error(), "fix conflicts") || strings.Contains(result.Error(), "Resolve all conflicts manually") {
return gui.createConfirmationPanel(gui.g, gui.getFilesView(), true, gui.Tr.SLocalize("FoundConflictsTitle"), gui.Tr.SLocalize("FoundConflicts"),
func(g *gocui.Gui, v *gocui.View) error {
@@ -83,6 +86,6 @@ func (gui *Gui) handleGenericMergeCommandResult(result error) error {
},
)
} else {
return gui.createErrorPanel(gui.g, result.Error())
return gui.createErrorPanel(result.Error())
}
}

View File

@@ -32,6 +32,7 @@ func (gui *Gui) handleCreateRecentReposMenu(g *gocui.Gui, v *gocui.View) error {
return err
}
gui.GitCommand = newGitCommand
gui.State.FilterPath = ""
return gui.Errors.ErrSwitchRepo
},
}

View File

@@ -10,11 +10,12 @@ import (
func (gui *Gui) getSelectedReflogCommit() *commands.Commit {
selectedLine := gui.State.Panels.ReflogCommits.SelectedLine
if selectedLine == -1 || len(gui.State.ReflogCommits) == 0 {
reflogComits := gui.State.FilteredReflogCommits
if selectedLine == -1 || len(reflogComits) == 0 {
return nil
}
return gui.State.ReflogCommits[selectedLine]
return reflogComits[selectedLine]
}
func (gui *Gui) handleReflogCommitSelect(g *gocui.Gui, v *gocui.View) error {
@@ -36,8 +37,12 @@ func (gui *Gui) handleReflogCommitSelect(g *gocui.Gui, v *gocui.View) error {
}
v.FocusPoint(0, gui.State.Panels.ReflogCommits.SelectedLine)
if gui.inDiffMode() {
return gui.renderDiff()
}
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowCmdStr(commit.Sha),
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.FilterPath),
)
if err := gui.newPtyTask("main", cmd); err != nil {
gui.Log.Error(err)
@@ -46,13 +51,47 @@ func (gui *Gui) handleReflogCommitSelect(g *gocui.Gui, v *gocui.View) error {
return nil
}
// the reflogs panel is the only panel where we cache data, in that we only
// load entries that have been created since we last ran the call. This means
// we need to be more careful with how we use this, and to ensure we're emptying
// the reflogs array when changing contexts.
// This method also manages two things: ReflogCommits and FilteredReflogCommits.
// FilteredReflogCommits are rendered in the reflogs panel, and ReflogCommits
// are used by the branches panel to obtain recency values for sorting.
func (gui *Gui) refreshReflogCommits() error {
commits, err := gui.GitCommand.GetReflogCommits()
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
// pulling state into its own variable incase it gets swapped out for another state
// and we get an out of bounds exception
state := gui.State
var lastReflogCommit *commands.Commit
if len(state.ReflogCommits) > 0 {
lastReflogCommit = state.ReflogCommits[0]
}
gui.State.ReflogCommits = commits
refresh := func(stateCommits *[]*commands.Commit, filterPath string) error {
commits, onlyObtainedNewReflogCommits, err := gui.GitCommand.GetReflogCommits(lastReflogCommit, filterPath)
if err != nil {
return gui.surfaceError(err)
}
if onlyObtainedNewReflogCommits {
*stateCommits = append(commits, *stateCommits...)
} else {
*stateCommits = commits
}
return nil
}
if err := refresh(&state.ReflogCommits, ""); err != nil {
return err
}
if gui.inFilterMode() {
if err := refresh(&state.FilteredReflogCommits, state.FilterPath); err != nil {
return err
}
} else {
state.FilteredReflogCommits = state.ReflogCommits
}
if gui.getCommitsView().Context == "reflog-commits" {
return gui.renderReflogCommitsWithSelection()
@@ -64,8 +103,8 @@ func (gui *Gui) refreshReflogCommits() error {
func (gui *Gui) renderReflogCommitsWithSelection() error {
commitsView := gui.getCommitsView()
gui.refreshSelectedLine(&gui.State.Panels.ReflogCommits.SelectedLine, len(gui.State.ReflogCommits))
displayStrings := presentation.GetCommitListDisplayStrings(gui.State.ReflogCommits, gui.State.ScreenMode != SCREEN_NORMAL)
gui.refreshSelectedLine(&gui.State.Panels.ReflogCommits.SelectedLine, len(gui.State.FilteredReflogCommits))
displayStrings := presentation.GetReflogCommitListDisplayStrings(gui.State.FilteredReflogCommits, gui.State.ScreenMode != SCREEN_NORMAL, gui.State.Diff.Ref)
gui.renderDisplayStrings(commitsView, displayStrings)
if gui.g.CurrentView() == commitsView && commitsView.Context == "reflog-commits" {
if err := gui.handleReflogCommitSelect(gui.g, commitsView); err != nil {
@@ -83,7 +122,7 @@ func (gui *Gui) handleCheckoutReflogCommit(g *gocui.Gui, v *gocui.View) error {
}
err := gui.createConfirmationPanel(g, gui.getCommitsView(), true, gui.Tr.SLocalize("checkoutCommit"), gui.Tr.SLocalize("SureCheckoutThisCommit"), func(g *gocui.Gui, v *gocui.View) error {
return gui.handleCheckoutRef(commit.Sha)
return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{})
}, nil)
if err != nil {
return err

View File

@@ -32,7 +32,6 @@ func (gui *Gui) handleRemoteBranchSelect(g *gocui.Gui, v *gocui.View) error {
gui.getMainView().Title = "Remote Branch"
remote := gui.getSelectedRemote()
remoteBranch := gui.getSelectedRemoteBranch()
if remoteBranch == nil {
return gui.newStringTask("main", "No branches for this remote")
@@ -40,10 +39,12 @@ func (gui *Gui) handleRemoteBranchSelect(g *gocui.Gui, v *gocui.View) error {
v.FocusPoint(0, gui.State.Panels.RemoteBranches.SelectedLine)
branchName := fmt.Sprintf("%s/%s", remote.Name, remoteBranch.Name)
if gui.inDiffMode() {
return gui.renderDiff()
}
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.GetBranchGraphCmdStr(branchName),
gui.GitCommand.GetBranchGraphCmdStr(remoteBranch.FullName()),
)
if err := gui.newCmdTask("main", cmd); err != nil {
gui.Log.Error(err)
@@ -60,7 +61,7 @@ func (gui *Gui) renderRemoteBranchesWithSelection() error {
branchesView := gui.getBranchesView()
gui.refreshSelectedLine(&gui.State.Panels.RemoteBranches.SelectedLine, len(gui.State.RemoteBranches))
displayStrings := presentation.GetRemoteBranchListDisplayStrings(gui.State.RemoteBranches)
displayStrings := presentation.GetRemoteBranchListDisplayStrings(gui.State.RemoteBranches, gui.State.Diff.Ref)
gui.renderDisplayStrings(branchesView, displayStrings)
if gui.g.CurrentView() == branchesView && branchesView.Context == "remote-branches" {
if err := gui.handleRemoteBranchSelect(gui.g, branchesView); err != nil {
@@ -76,7 +77,7 @@ func (gui *Gui) handleCheckoutRemoteBranch(g *gocui.Gui, v *gocui.View) error {
if remoteBranch == nil {
return nil
}
if err := gui.handleCheckoutRef(remoteBranch.RemoteName + "/" + remoteBranch.Name); err != nil {
if err := gui.handleCheckoutRef(remoteBranch.FullName(), handleCheckoutRefOptions{}); err != nil {
return err
}
return gui.switchBranchesPanelContext("local-branches")
@@ -99,7 +100,7 @@ func (gui *Gui) handleDeleteRemoteBranch(g *gocui.Gui, v *gocui.View) error {
return err
}
return gui.refreshRemotes()
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
})
}, nil)
}
@@ -117,7 +118,7 @@ func (gui *Gui) handleSetBranchUpstream(g *gocui.Gui, v *gocui.View) error {
"SetUpstreamMessage",
Teml{
"checkedOut": checkedOutBranch.Name,
"selected": selectedBranch.RemoteName + "/" + selectedBranch.Name,
"selected": selectedBranch.FullName(),
},
)
@@ -126,7 +127,7 @@ func (gui *Gui) handleSetBranchUpstream(g *gocui.Gui, v *gocui.View) error {
return err
}
return gui.refreshSidePanels(gui.g)
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
}, nil)
}

View File

@@ -41,6 +41,10 @@ func (gui *Gui) handleRemoteSelect(g *gocui.Gui, v *gocui.View) error {
}
v.FocusPoint(0, gui.State.Panels.Remotes.SelectedLine)
if gui.inDiffMode() {
return gui.renderDiff()
}
return gui.newStringTask("main", fmt.Sprintf("%s\nUrls:\n%s", utils.ColoredString(remote.Name, color.FgGreen), strings.Join(remote.Urls, "\n")))
}
@@ -49,7 +53,7 @@ func (gui *Gui) refreshRemotes() error {
remotes, err := gui.GitCommand.GetRemotes()
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
gui.State.Remotes = remotes
@@ -80,7 +84,7 @@ func (gui *Gui) renderRemotesWithSelection() error {
gui.refreshSelectedLine(&gui.State.Panels.Remotes.SelectedLine, len(gui.State.Remotes))
displayStrings := presentation.GetRemoteListDisplayStrings(gui.State.Remotes)
displayStrings := presentation.GetRemoteListDisplayStrings(gui.State.Remotes, gui.State.Diff.Ref)
gui.renderDisplayStrings(branchesView, displayStrings)
if gui.g.CurrentView() == branchesView && branchesView.Context == "remotes" {
@@ -119,7 +123,7 @@ func (gui *Gui) handleAddRemote(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.AddRemote(remoteName, remoteUrl); err != nil {
return err
}
return gui.refreshRemotes()
return gui.refreshSidePanels(refreshOptions{scope: []int{REMOTES}})
})
})
}
@@ -134,7 +138,7 @@ func (gui *Gui) handleRemoveRemote(g *gocui.Gui, v *gocui.View) error {
return err
}
return gui.refreshRemotes()
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
}, nil)
}
@@ -158,7 +162,7 @@ func (gui *Gui) handleEditRemote(g *gocui.Gui, v *gocui.View) error {
if updatedRemoteName != remote.Name {
if err := gui.GitCommand.RenameRemote(remote.Name, updatedRemoteName); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
}
@@ -172,9 +176,9 @@ func (gui *Gui) handleEditRemote(g *gocui.Gui, v *gocui.View) error {
return gui.createPromptPanel(g, branchesView, editUrlMessage, "", func(g *gocui.Gui, v *gocui.View) error {
updatedRemoteUrl := gui.trimmedContent(v)
if err := gui.GitCommand.UpdateRemoteUrl(updatedRemoteName, updatedRemoteUrl); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
return gui.refreshRemotes()
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
})
})
}
@@ -190,6 +194,6 @@ func (gui *Gui) handleFetchRemote(g *gocui.Gui, v *gocui.View) error {
return err
}
return gui.refreshRemotes()
return gui.refreshSidePanels(refreshOptions{scope: []int{BRANCHES, REMOTES}})
})
}

View File

@@ -4,13 +4,38 @@ import (
"fmt"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/commands"
)
func (gui *Gui) resetToRef(ref string, strength string, options commands.RunCommandOptions) error {
if err := gui.GitCommand.ResetToCommit(ref, strength, options); err != nil {
return gui.surfaceError(err)
}
if err := gui.switchCommitsPanelContext("branch-commits"); err != nil {
return err
}
gui.State.Panels.Commits.SelectedLine = 0
gui.State.Panels.ReflogCommits.SelectedLine = 0
// loading a heap of commits is slow so we limit them whenever doing a reset
gui.State.Panels.Commits.LimitCommits = true
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES, BRANCHES, REFLOG, COMMITS}}); err != nil {
return err
}
if err := gui.resetOrigin(gui.getCommitsView()); err != nil {
return err
}
return gui.handleCommitSelect(gui.g, gui.getCommitsView())
}
func (gui *Gui) createResetMenu(ref string) error {
strengths := []string{"soft", "mixed", "hard"}
menuItems := make([]*menuItem, len(strengths))
for i, strength := range strengths {
innerStrength := strength
strength := strength
menuItems[i] = &menuItem{
displayStrings: []string{
fmt.Sprintf("%s reset", strength),
@@ -19,31 +44,7 @@ func (gui *Gui) createResetMenu(ref string) error {
),
},
onPress: func() error {
if err := gui.GitCommand.ResetToCommit(ref, innerStrength); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
if err := gui.switchCommitsPanelContext("branch-commits"); err != nil {
return err
}
gui.State.Panels.Commits.SelectedLine = 0
gui.State.Panels.ReflogCommits.SelectedLine = 0
if err := gui.refreshCommits(gui.g); err != nil {
return err
}
if err := gui.refreshFiles(); err != nil {
return err
}
if err := gui.refreshBranches(gui.g); err != nil {
return err
}
if err := gui.resetOrigin(gui.getCommitsView()); err != nil {
return err
}
return gui.handleCommitSelect(gui.g, gui.getCommitsView())
return gui.resetToRef(ref, strength, commands.RunCommandOptions{})
},
}
}

View File

@@ -19,7 +19,7 @@ func (gui *Gui) refreshStagingPanel(forceSecondaryFocused bool, selectedLineIdx
return err
}
file, err := gui.getSelectedFile(gui.g)
file, err := gui.getSelectedFile()
if err != nil {
if err != gui.Errors.ErrNoFiles {
return err
@@ -125,7 +125,7 @@ func (gui *Gui) handleResetSelection(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) applySelection(reverse bool) error {
state := gui.State.Panels.LineByLine
file, err := gui.getSelectedFile(gui.g)
file, err := gui.getSelectedFile()
if err != nil {
return err
}
@@ -144,14 +144,14 @@ func (gui *Gui) applySelection(reverse bool) error {
}
err = gui.GitCommand.ApplyPatch(patch, applyFlags...)
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
if state.SelectMode == RANGE {
state.SelectMode = LINE
}
if err := gui.refreshFiles(); err != nil {
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
return err
}
if err := gui.refreshStagingPanel(false, -1); err != nil {

View File

@@ -8,7 +8,7 @@ import (
// list panel functions
func (gui *Gui) getSelectedStashEntry(v *gocui.View) *commands.StashEntry {
func (gui *Gui) getSelectedStashEntry() *commands.StashEntry {
selectedLine := gui.State.Panels.Stash.SelectedLine
if selectedLine == -1 {
return nil
@@ -30,12 +30,16 @@ func (gui *Gui) handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error {
gui.getMainView().Title = "Stash"
stashEntry := gui.getSelectedStashEntry(v)
stashEntry := gui.getSelectedStashEntry()
if stashEntry == nil {
return gui.newStringTask("main", gui.Tr.SLocalize("NoStashEntries"))
}
v.FocusPoint(0, gui.State.Panels.Stash.SelectedLine)
if gui.inDiffMode() {
return gui.renderDiff()
}
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowStashEntryCmdStr(stashEntry.Index),
)
@@ -47,22 +51,16 @@ func (gui *Gui) handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) refreshStashEntries(g *gocui.Gui) error {
g.Update(func(g *gocui.Gui) error {
gui.State.StashEntries = gui.GitCommand.GetStashEntries()
gui.State.StashEntries = gui.GitCommand.GetStashEntries(gui.State.FilterPath)
gui.refreshSelectedLine(&gui.State.Panels.Stash.SelectedLine, len(gui.State.StashEntries))
gui.refreshSelectedLine(&gui.State.Panels.Stash.SelectedLine, len(gui.State.StashEntries))
stashView := gui.getStashView()
stashView := gui.getStashView()
displayStrings := presentation.GetStashEntryListDisplayStrings(gui.State.StashEntries)
gui.renderDisplayStrings(stashView, displayStrings)
displayStrings := presentation.GetStashEntryListDisplayStrings(gui.State.StashEntries, gui.State.Diff.Ref)
gui.renderDisplayStrings(stashView, displayStrings)
if err := gui.resetOrigin(stashView); err != nil {
return err
}
return nil
})
return nil
return gui.resetOrigin(stashView)
}
// specific functions
@@ -84,7 +82,7 @@ func (gui *Gui) handleStashDrop(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) stashDo(g *gocui.Gui, v *gocui.View, method string) error {
stashEntry := gui.getSelectedStashEntry(v)
stashEntry := gui.getSelectedStashEntry()
if stashEntry == nil {
errorMessage := gui.Tr.TemplateLocalize(
"NoStashTo",
@@ -92,29 +90,23 @@ func (gui *Gui) stashDo(g *gocui.Gui, v *gocui.View, method string) error {
"method": method,
},
)
return gui.createErrorPanel(g, errorMessage)
return gui.createErrorPanel(errorMessage)
}
if err := gui.GitCommand.StashDo(stashEntry.Index, method); err != nil {
return gui.createErrorPanel(g, err.Error())
return gui.surfaceError(err)
}
if err := gui.refreshStashEntries(g); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return gui.refreshFiles()
return gui.refreshSidePanels(refreshOptions{scope: []int{STASH, FILES}})
}
func (gui *Gui) handleStashSave(stashFunc func(message string) error) error {
if len(gui.trackedFiles()) == 0 && len(gui.stagedFiles()) == 0 {
return gui.createErrorPanel(gui.g, gui.Tr.SLocalize("NoTrackedStagedFilesStash"))
return gui.createErrorPanel(gui.Tr.SLocalize("NoTrackedStagedFilesStash"))
}
return gui.createPromptPanel(gui.g, gui.getFilesView(), gui.Tr.SLocalize("StashChanges"), "", func(g *gocui.Gui, v *gocui.View) error {
if err := stashFunc(gui.trimmedContent(v)); err != nil {
return gui.createErrorPanel(g, err.Error())
return gui.surfaceError(err)
}
if err := gui.refreshStashEntries(g); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return gui.refreshFiles()
return gui.refreshSidePanels(refreshOptions{scope: []int{STASH, FILES}})
})
}

View File

@@ -10,50 +10,41 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) refreshStatus(g *gocui.Gui) error {
state := gui.State.Panels.Status
// never call this on its own, it should only be called from within refreshCommits()
func (gui *Gui) refreshStatus() {
gui.State.RefreshingStatusMutex.Lock()
defer gui.State.RefreshingStatusMutex.Unlock()
v, err := g.View("status")
if err != nil {
panic(err)
currentBranch := gui.currentBranch()
if currentBranch == nil {
// need to wait for branches to refresh
return
}
// for some reason if this isn't wrapped in an update the clear seems to
// be applied after the other things or something like that; the panel's
// contents end up cleared
g.Update(func(*gocui.Gui) error {
v.Clear()
// TODO: base this off of the current branch
state.pushables, state.pullables = gui.GitCommand.GetCurrentBranchUpstreamDifferenceCount()
if err := gui.updateWorkTreeState(); err != nil {
return err
}
status := ""
if currentBranch.Pushables != "" && currentBranch.Pullables != "" {
trackColor := color.FgYellow
if state.pushables == "0" && state.pullables == "0" {
if currentBranch.Pushables == "0" && currentBranch.Pullables == "0" {
trackColor = color.FgGreen
} else if state.pushables == "?" && state.pullables == "?" {
} else if currentBranch.Pushables == "?" && currentBranch.Pullables == "?" {
trackColor = color.FgRed
}
status := utils.ColoredString(fmt.Sprintf("↑%s↓%s", state.pushables, state.pullables), trackColor)
branches := gui.State.Branches
status = utils.ColoredString(fmt.Sprintf("↑%s↓%s ", currentBranch.Pushables, currentBranch.Pullables), trackColor)
}
if gui.State.WorkingTreeState != "normal" {
status += utils.ColoredString(fmt.Sprintf(" (%s)", gui.State.WorkingTreeState), color.FgYellow)
}
if gui.GitCommand.WorkingTreeState() != "normal" {
status += utils.ColoredString(fmt.Sprintf("(%s) ", gui.GitCommand.WorkingTreeState()), color.FgYellow)
}
if len(branches) > 0 {
branch := branches[0]
name := utils.ColoredString(branch.Name, presentation.GetBranchColor(branch.Name))
repoName := utils.GetCurrentRepoName()
status += fmt.Sprintf(" %s → %s", repoName, name)
}
name := utils.ColoredString(currentBranch.Name, presentation.GetBranchColor(currentBranch.Name))
repoName := utils.GetCurrentRepoName()
status += fmt.Sprintf("%s → %s ", repoName, name)
fmt.Fprint(v, status)
gui.g.Update(func(*gocui.Gui) error {
gui.setViewContent(gui.g, gui.getStatusView(), status)
return nil
})
return nil
}
func runeCount(str string) int {
@@ -70,15 +61,14 @@ func (gui *Gui) handleCheckForUpdate(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleStatusClick(g *gocui.Gui, v *gocui.View) error {
state := gui.State.Panels.Status
currentBranch := gui.currentBranch()
cx, _ := v.Cursor()
upstreamStatus := fmt.Sprintf("↑%s↓%s", state.pushables, state.pullables)
upstreamStatus := fmt.Sprintf("↑%s↓%s", currentBranch.Pushables, currentBranch.Pullables)
repoName := utils.GetCurrentRepoName()
gui.Log.Warn(gui.State.WorkingTreeState)
switch gui.State.WorkingTreeState {
switch gui.GitCommand.WorkingTreeState() {
case "rebasing", "merging":
workingTreeStatus := fmt.Sprintf("(%s)", gui.State.WorkingTreeState)
workingTreeStatus := fmt.Sprintf("(%s)", gui.GitCommand.WorkingTreeState())
if cursorInSubstring(cx, upstreamStatus+" ", workingTreeStatus) {
return gui.handleCreateRebaseOptionsMenu(gui.g, v)
}
@@ -107,6 +97,10 @@ func (gui *Gui) handleStatusSelect(g *gocui.Gui, v *gocui.View) error {
gui.getMainView().Title = ""
if gui.inDiffMode() {
return gui.renderDiff()
}
magenta := color.New(color.FgMagenta)
dashboardString := strings.Join(
@@ -144,23 +138,14 @@ func lazygitTitle() string {
|___/ |___/ `
}
func (gui *Gui) updateWorkTreeState() error {
rebaseMode, err := gui.GitCommand.RebaseMode()
if err != nil {
return err
}
func (gui *Gui) workingTreeState() string {
rebaseMode, _ := gui.GitCommand.RebaseMode()
if rebaseMode != "" {
gui.State.WorkingTreeState = "rebasing"
return nil
}
merging, err := gui.GitCommand.IsInMergeState()
if err != nil {
return err
return "rebasing"
}
merging, _ := gui.GitCommand.IsInMergeState()
if merging {
gui.State.WorkingTreeState = "merging"
return nil
return "merging"
}
gui.State.WorkingTreeState = "normal"
return nil
return "normal"
}

View File

@@ -36,6 +36,10 @@ func (gui *Gui) handleTagSelect(g *gocui.Gui, v *gocui.View) error {
}
v.FocusPoint(0, gui.State.Panels.Tags.SelectedLine)
if gui.inDiffMode() {
return gui.renderDiff()
}
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.GetBranchGraphCmdStr(tag.Name),
)
@@ -49,7 +53,7 @@ func (gui *Gui) handleTagSelect(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) refreshTags() error {
tags, err := gui.GitCommand.GetTags()
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
gui.State.Tags = tags
@@ -65,11 +69,11 @@ func (gui *Gui) renderTagsWithSelection() error {
branchesView := gui.getBranchesView()
gui.refreshSelectedLine(&gui.State.Panels.Tags.SelectedLine, len(gui.State.Tags))
displayStrings := presentation.GetTagListDisplayStrings(gui.State.Tags)
displayStrings := presentation.GetTagListDisplayStrings(gui.State.Tags, gui.State.Diff.Ref)
gui.renderDisplayStrings(branchesView, displayStrings)
if gui.g.CurrentView() == branchesView && branchesView.Context == "tags" {
if err := gui.handleTagSelect(gui.g, branchesView); err != nil {
return err
return gui.surfaceError(err)
}
}
@@ -81,7 +85,7 @@ func (gui *Gui) handleCheckoutTag(g *gocui.Gui, v *gocui.View) error {
if tag == nil {
return nil
}
if err := gui.handleCheckoutRef(tag.Name); err != nil {
if err := gui.handleCheckoutRef(tag.Name, handleCheckoutRefOptions{}); err != nil {
return err
}
return gui.switchBranchesPanelContext("local-branches")
@@ -102,15 +106,9 @@ func (gui *Gui) handleDeleteTag(g *gocui.Gui, v *gocui.View) error {
return gui.createConfirmationPanel(gui.g, v, true, gui.Tr.SLocalize("DeleteTagTitle"), prompt, func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.DeleteTag(tag.Name); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
if err := gui.refreshCommits(g); err != nil {
return gui.createErrorPanel(g, err.Error())
}
if err := gui.refreshTags(); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return nil
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{COMMITS, TAGS}})
}, nil)
}
@@ -129,25 +127,30 @@ func (gui *Gui) handlePushTag(g *gocui.Gui, v *gocui.View) error {
return gui.createPromptPanel(gui.g, v, title, "origin", func(g *gocui.Gui, v *gocui.View) error {
if err := gui.GitCommand.PushTag(v.Buffer(), tag.Name); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
return gui.refreshTags()
return nil
})
}
func (gui *Gui) handleCreateTag(g *gocui.Gui, v *gocui.View) error {
return gui.createPromptPanel(gui.g, v, gui.Tr.SLocalize("CreateTagTitle"), "", func(g *gocui.Gui, v *gocui.View) error {
// leaving commit SHA blank so that we're just creating the tag for the current commit
if err := gui.GitCommand.CreateLightweightTag(v.Buffer(), ""); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
tagName := v.Buffer()
if err := gui.GitCommand.CreateLightweightTag(tagName, ""); err != nil {
return gui.surfaceError(err)
}
if err := gui.refreshCommits(g); err != nil {
return gui.createErrorPanel(g, err.Error())
}
if err := gui.refreshTags(); err != nil {
return gui.createErrorPanel(g, err.Error())
}
return nil
return gui.refreshSidePanels(refreshOptions{scope: []int{COMMITS, TAGS}, then: func() {
// find the index of the tag and set that as the currently selected line
for i, tag := range gui.State.Tags {
if tag.Name == tagName {
gui.State.Panels.Tags.SelectedLine = i
gui.renderTagsWithSelection()
return
}
}
},
})
})
}

View File

@@ -81,7 +81,6 @@ func (gui *Gui) getManager(view *gocui.View) *tasks.ViewBufferManager {
},
func() {
gui.g.Update(func(*gocui.Gui) error {
gui.Log.Warn("updating view")
return nil
})
})

192
pkg/gui/undoing.go Normal file
View File

@@ -0,0 +1,192 @@
package gui
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// Quick summary of how this all works:
// when you want to undo or redo, we start from the top of the reflog and work
// down until we've reached the last user-initiated reflog entry that hasn't already been undone
// we then do the reverse of what that reflog describes.
// When we do this, we create a new reflog entry, and tag it as either an undo or redo
// Then, next time we want to undo, we'll use those entries to know which user-initiated
// actions we can skip. E.g. if I do do three things, A, B, and C, and hit undo twice,
// the reflog will read UUCBA, and when I read the first two undos, I know to skip the following
// two user actions, meaning we end up undoing reflog entry C. Redoing works in a similar way.
const (
CHECKOUT = iota
COMMIT
REBASE
CURRENT_REBASE
)
type reflogAction struct {
kind int // one of CHECKOUT, REBASE, and COMMIT
from string
to string
}
// Here we're going through the reflog and maintaining a counter that represents how many
// undos/redos/user actions we've seen. when we hit a user action we call the callback specifying
// what the counter is up to and the nature of the action.
// If we find ourselves mid-rebase, we just return because undo/redo mid rebase
// requires knowledge of previous TODO file states, which you can't just get from the reflog.
// Though we might support this later, hence the use of the CURRENT_REBASE action kind.
func (gui *Gui) parseReflogForActions(onUserAction func(counter int, action reflogAction) (bool, error)) error {
counter := 0
reflogCommits := gui.State.FilteredReflogCommits
rebaseFinishCommitSha := ""
var action *reflogAction
for reflogCommitIdx, reflogCommit := range reflogCommits {
action = nil
prevCommitSha := ""
if len(reflogCommits)-1 >= reflogCommitIdx+1 {
prevCommitSha = reflogCommits[reflogCommitIdx+1].Sha
}
if rebaseFinishCommitSha == "" {
if ok, _ := utils.FindStringSubmatch(reflogCommit.Name, `^\[lazygit undo\]`); ok {
counter++
} else if ok, _ := utils.FindStringSubmatch(reflogCommit.Name, `^\[lazygit redo\]`); ok {
counter--
} else if ok, _ := utils.FindStringSubmatch(reflogCommit.Name, `^rebase -i \(abort\)|^rebase -i \(finish\)`); ok {
rebaseFinishCommitSha = reflogCommit.Sha
} else if ok, match := utils.FindStringSubmatch(reflogCommit.Name, `^checkout: moving from ([\S]+) to ([\S]+)`); ok {
action = &reflogAction{kind: CHECKOUT, from: match[1], to: match[2]}
} else if ok, _ := utils.FindStringSubmatch(reflogCommit.Name, `^commit|^reset: moving to|^pull`); ok {
action = &reflogAction{kind: COMMIT, from: prevCommitSha, to: reflogCommit.Sha}
} else if ok, _ := utils.FindStringSubmatch(reflogCommit.Name, `^rebase -i \(start\)`); ok {
// if we're here then we must be currently inside an interactive rebase
action = &reflogAction{kind: CURRENT_REBASE, from: prevCommitSha}
}
} else if ok, _ := utils.FindStringSubmatch(reflogCommit.Name, `^rebase -i \(start\)`); ok {
action = &reflogAction{kind: REBASE, from: prevCommitSha, to: rebaseFinishCommitSha}
rebaseFinishCommitSha = ""
}
if action != nil {
if action.kind != CURRENT_REBASE && action.from == action.to {
// if we're going from one place to the same place we'll ignore the action.
continue
}
ok, err := onUserAction(counter, *action)
if ok {
return err
}
counter--
}
}
return nil
}
func (gui *Gui) reflogUndo(g *gocui.Gui, v *gocui.View) error {
undoEnvVars := []string{"GIT_REFLOG_ACTION=[lazygit undo]"}
undoingStatus := gui.Tr.SLocalize("UndoingStatus")
if gui.GitCommand.WorkingTreeState() == "rebasing" {
return gui.createErrorPanel(gui.Tr.SLocalize("cantUndoWhileRebasing"))
}
return gui.parseReflogForActions(func(counter int, action reflogAction) (bool, error) {
if counter != 0 {
return false, nil
}
switch action.kind {
case COMMIT, REBASE:
return true, gui.handleHardResetWithAutoStash(action.from, handleHardResetWithAutoStashOptions{
EnvVars: undoEnvVars,
WaitingStatus: undoingStatus,
})
case CHECKOUT:
return true, gui.handleCheckoutRef(action.from, handleCheckoutRefOptions{
EnvVars: undoEnvVars,
WaitingStatus: undoingStatus,
})
}
gui.Log.Error("didn't match on the user action when trying to undo")
return true, nil
})
}
func (gui *Gui) reflogRedo(g *gocui.Gui, v *gocui.View) error {
redoEnvVars := []string{"GIT_REFLOG_ACTION=[lazygit redo]"}
redoingStatus := gui.Tr.SLocalize("RedoingStatus")
if gui.GitCommand.WorkingTreeState() == "rebasing" {
return gui.createErrorPanel(gui.Tr.SLocalize("cantRedoWhileRebasing"))
}
return gui.parseReflogForActions(func(counter int, action reflogAction) (bool, error) {
// if we're redoing and the counter is zero, we just return
if counter == 0 {
return true, nil
} else if counter > 1 {
return false, nil
}
switch action.kind {
case COMMIT, REBASE:
return true, gui.handleHardResetWithAutoStash(action.to, handleHardResetWithAutoStashOptions{
EnvVars: redoEnvVars,
WaitingStatus: redoingStatus,
})
case CHECKOUT:
return true, gui.handleCheckoutRef(action.to, handleCheckoutRefOptions{
EnvVars: redoEnvVars,
WaitingStatus: redoingStatus,
})
}
gui.Log.Error("didn't match on the user action when trying to redo")
return true, nil
})
}
type handleHardResetWithAutoStashOptions struct {
WaitingStatus string
EnvVars []string
}
// only to be used in the undo flow for now
func (gui *Gui) handleHardResetWithAutoStash(commitSha string, options handleHardResetWithAutoStashOptions) error {
reset := func() error {
if err := gui.resetToRef(commitSha, "hard", commands.RunCommandOptions{EnvVars: options.EnvVars}); err != nil {
return gui.surfaceError(err)
}
return nil
}
// if we have any modified tracked files we need to ask the user if they want us to stash for them
dirtyWorkingTree := len(gui.trackedFiles()) > 0
if dirtyWorkingTree {
// 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 {
return gui.WithWaitingStatus(options.WaitingStatus, func() error {
if err := gui.GitCommand.StashSave(gui.Tr.SLocalize("StashPrefix") + commitSha); err != nil {
return gui.surfaceError(err)
}
if err := reset(); err != nil {
return err
}
if err := gui.GitCommand.StashDo(0, "pop"); err != nil {
if err := gui.refreshSidePanels(refreshOptions{}); err != nil {
return err
}
return gui.surfaceError(err)
}
return nil
})
}, nil)
}
return gui.WithWaitingStatus(options.WaitingStatus, func() error {
return reset()
})
}

View File

@@ -14,10 +14,10 @@ func (gui *Gui) showUpdatePrompt(newVersion string) error {
func (gui *Gui) onUserUpdateCheckFinish(newVersion string, err error) error {
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
if newVersion == "" {
return gui.createErrorPanel(gui.g, "New version not found")
return gui.createErrorPanel("New version not found")
}
return gui.showUpdatePrompt(newVersion)
}
@@ -49,7 +49,7 @@ func (gui *Gui) onUpdateFinish(err error) error {
gui.statusManager.removeStatus("updating")
gui.renderString(gui.g, "appStatus", "")
if err != nil {
return gui.createErrorPanel(gui.g, "Update failed: "+err.Error())
return gui.createErrorPanel("Update failed: " + err.Error())
}
return nil
}

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"sort"
"strings"
"sync"
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
@@ -13,18 +14,128 @@ import (
var cyclableViews = []string{"status", "files", "branches", "commits", "stash"}
func (gui *Gui) refreshSidePanels(g *gocui.Gui) error {
if err := gui.refreshBranches(g); err != nil {
return err
// models/views that we can refresh
const (
COMMITS = iota
BRANCHES
FILES
STASH
REFLOG
TAGS
REMOTES
STATUS
)
const (
SYNC = iota // wait until everything is done before returning
ASYNC // return immediately, allowing each independent thing to update itself
BLOCK_UI // wrap code in an update call to ensure UI updates all at once and keybindings aren't executed till complete
)
type refreshOptions struct {
then func()
scope []int // e.g. []int{COMMITS, BRANCHES}. Leave empty to refresh everything
mode int // one of SYNC (default), ASYNC, and BLOCK_UI
}
func intArrToMap(arr []int) map[int]bool {
output := map[int]bool{}
for _, el := range arr {
output[el] = true
}
if err := gui.refreshFiles(); err != nil {
return err
}
if err := gui.refreshCommits(g); err != nil {
return err
return output
}
func (gui *Gui) refreshSidePanels(options refreshOptions) error {
wg := sync.WaitGroup{}
f := func() {
var scopeMap map[int]bool
if len(options.scope) == 0 {
scopeMap = intArrToMap([]int{COMMITS, BRANCHES, FILES, STASH, REFLOG, TAGS, REMOTES, STATUS})
} else {
scopeMap = intArrToMap(options.scope)
}
if scopeMap[COMMITS] || scopeMap[BRANCHES] || scopeMap[REFLOG] {
wg.Add(1)
func() {
if options.mode == ASYNC {
go gui.refreshCommits()
} else {
gui.refreshCommits()
}
wg.Done()
}()
}
if scopeMap[FILES] {
wg.Add(1)
func() {
if options.mode == ASYNC {
go gui.refreshFiles()
} else {
gui.refreshFiles()
}
wg.Done()
}()
}
if scopeMap[STASH] {
wg.Add(1)
func() {
if options.mode == ASYNC {
go gui.refreshStashEntries(gui.g)
} else {
gui.refreshStashEntries(gui.g)
}
wg.Done()
}()
}
if scopeMap[TAGS] {
wg.Add(1)
func() {
if options.mode == ASYNC {
go gui.refreshTags()
} else {
gui.refreshTags()
}
wg.Done()
}()
}
if scopeMap[REMOTES] {
wg.Add(1)
func() {
if options.mode == ASYNC {
go gui.refreshRemotes()
} else {
gui.refreshRemotes()
}
wg.Done()
}()
}
wg.Wait()
gui.refreshStatus()
if options.then != nil {
options.then()
}
}
return gui.refreshStashEntries(g)
if options.mode == BLOCK_UI {
gui.g.Update(func(g *gocui.Gui) error {
f()
return nil
})
} else {
f()
}
return nil
}
func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error {
@@ -58,6 +169,9 @@ func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error {
if err != nil {
panic(err)
}
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
return gui.switchFocus(g, v, focusedView)
}
@@ -92,6 +206,9 @@ func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error {
if err != nil {
panic(err)
}
if err := gui.resetOrigin(gui.getMainView()); err != nil {
return err
}
return gui.switchFocus(g, v, focusedView)
}
@@ -312,6 +429,11 @@ func (gui *Gui) getSearchView() *gocui.View {
return v
}
func (gui *Gui) getStatusView() *gocui.View {
v, _ := gui.g.View("status")
return v
}
func (gui *Gui) trimmedContent(v *gocui.View) string {
return strings.TrimSpace(v.Buffer())
}
@@ -387,6 +509,16 @@ func (gui *Gui) renderPanelOptions() error {
return gui.renderGlobalOptions()
}
func (gui *Gui) renderGlobalOptions() error {
return gui.renderOptionsMap(map[string]string{
fmt.Sprintf("%s/%s", gui.getKeyDisplay("universal.scrollUpMain"), gui.getKeyDisplay("universal.scrollDownMain")): gui.Tr.SLocalize("scroll"),
fmt.Sprintf("%s %s %s %s", gui.getKeyDisplay("universal.prevBlock"), gui.getKeyDisplay("universal.nextBlock"), gui.getKeyDisplay("universal.prevItem"), gui.getKeyDisplay("universal.nextItem")): gui.Tr.SLocalize("navigate"),
fmt.Sprintf("%s/%s", gui.getKeyDisplay("universal.return"), gui.getKeyDisplay("universal.quit")): gui.Tr.SLocalize("close"),
gui.getKeyDisplay("universal.optionMenu"): gui.Tr.SLocalize("menu"),
"1-5": gui.Tr.SLocalize("jump"),
})
}
func (gui *Gui) isPopupPanel(viewName string) bool {
return viewName == "commitMessage" || viewName == "credentials" || viewName == "confirmation" || viewName == "menu"
}

View File

@@ -16,10 +16,10 @@ func (gui *Gui) handleCreateResetMenu(g *gocui.Gui, v *gocui.View) error {
},
onPress: func() error {
if err := gui.GitCommand.ResetAndClean(); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
return gui.refreshFiles()
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
},
},
{
@@ -29,10 +29,10 @@ func (gui *Gui) handleCreateResetMenu(g *gocui.Gui, v *gocui.View) error {
},
onPress: func() error {
if err := gui.GitCommand.DiscardAnyUnstagedFileChanges(); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
return gui.refreshFiles()
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
},
},
{
@@ -42,10 +42,10 @@ func (gui *Gui) handleCreateResetMenu(g *gocui.Gui, v *gocui.View) error {
},
onPress: func() error {
if err := gui.GitCommand.RemoveUntrackedFiles(); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
return gui.refreshFiles()
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
},
},
{
@@ -55,10 +55,10 @@ func (gui *Gui) handleCreateResetMenu(g *gocui.Gui, v *gocui.View) error {
},
onPress: func() error {
if err := gui.GitCommand.ResetSoft("HEAD"); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
return gui.refreshFiles()
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
},
},
{
@@ -68,10 +68,10 @@ func (gui *Gui) handleCreateResetMenu(g *gocui.Gui, v *gocui.View) error {
},
onPress: func() error {
if err := gui.GitCommand.ResetSoft("HEAD"); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
return gui.refreshFiles()
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
},
},
{
@@ -81,10 +81,10 @@ func (gui *Gui) handleCreateResetMenu(g *gocui.Gui, v *gocui.View) error {
},
onPress: func() error {
if err := gui.GitCommand.ResetHard("HEAD"); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
return gui.surfaceError(err)
}
return gui.refreshFiles()
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []int{FILES}})
},
},
}

View File

@@ -36,12 +36,6 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "CommitsTitle",
Other: "Commits",
}, &i18n.Message{
ID: "CommitsDiffTitle",
Other: "Commits (specific diff mode)",
}, &i18n.Message{
ID: "CommitsDiff",
Other: "select commit to diff with another commit",
}, &i18n.Message{
ID: "StashTitle",
Other: "Stash",
@@ -360,6 +354,12 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "undo",
Other: "undo",
}, &i18n.Message{
ID: "undoReflog",
Other: "undo (via reflog) (experimental)",
}, &i18n.Message{
ID: "redoReflog",
Other: "redo (via reflog) (experimental)",
}, &i18n.Message{
ID: "pop",
Other: "pop",
@@ -741,6 +741,15 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "CherryPickingStatus",
Other: "cherry-picking",
}, &i18n.Message{
ID: "UndoingStatus",
Other: "undoing",
}, &i18n.Message{
ID: "RedoingStatus",
Other: "redoing",
}, &i18n.Message{
ID: "CheckingOutStatus",
Other: "checking out",
}, &i18n.Message{
ID: "CommitFiles",
Other: "Commit files",
@@ -1032,6 +1041,96 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{
ID: "closeMenu",
Other: "close menu",
}, &i18n.Message{
ID: "resetCherryPick",
Other: "reset cherry-picked (copied) commits selection",
}, &i18n.Message{
ID: "nextTab",
Other: "next tab",
}, &i18n.Message{
ID: "prevTab",
Other: "previous tab",
}, &i18n.Message{
ID: "cantUndoWhileRebasing",
Other: "Can't undo while rebasing",
}, &i18n.Message{
ID: "cantRedoWhileRebasing",
Other: "Can't redo while rebasing",
}, &i18n.Message{
ID: "MustStashWarning",
Other: "Pulling a patch out into the index requires stashing and unstashing your changes. If something goes wrong, you'll be able to access your files from the stash. Continue?",
}, &i18n.Message{
ID: "MustStashTitle",
Other: "Must stash",
}, &i18n.Message{
ID: "ConfirmationTitle",
Other: "Confirmation Panel",
}, &i18n.Message{
ID: "prevPage",
Other: "previous page",
}, &i18n.Message{
ID: "nextPage",
Other: "next page",
}, &i18n.Message{
ID: "gotoTop",
Other: "scroll to top",
}, &i18n.Message{
ID: "gotoBottom",
Other: "scroll to bottom",
}, &i18n.Message{
ID: "filteringBy",
Other: "filtering by",
}, &i18n.Message{
ID: "(reset)",
Other: "(reset)",
}, &i18n.Message{
ID: "openScopingMenu",
Other: "view scoping options",
}, &i18n.Message{
ID: "filterBy",
Other: "filter by",
}, &i18n.Message{
ID: "exitFilterMode",
Other: "stop filtering by path",
}, &i18n.Message{
ID: "filterPathOption",
Other: "enter path to filter by",
}, &i18n.Message{
ID: "enterFileName",
Other: "enter path:",
}, &i18n.Message{
ID: "FilteringMenuTitle",
Other: "Filtering",
}, &i18n.Message{
ID: "MustExitFilterModeTitle",
Other: "Command not available",
}, &i18n.Message{
ID: "MustExitFilterModePrompt",
Other: "Command not available in filtered mode. Exit filtered mode?",
}, &i18n.Message{
ID: "diff",
Other: "diff",
}, &i18n.Message{
ID: "enterRefToDiff",
Other: "enter ref to diff",
}, &i18n.Message{
ID: "enteRefName",
Other: "enter ref:",
}, &i18n.Message{
ID: "exitDiffMode",
Other: "exit diff mode",
}, &i18n.Message{
ID: "DiffingMenuTitle",
Other: "Diffing",
}, &i18n.Message{
ID: "swapDiff",
Other: "reverse diff direction",
}, &i18n.Message{
ID: "openDiffingMenu",
Other: "open diff menu",
}, &i18n.Message{
ID: "showingGitDiff",
Other: "showing output for:",
},
)
}

View File

@@ -27,6 +27,8 @@ var (
OptionsFgColor color.Attribute
OptionsColor gocui.Attribute
DiffTerminalColor = color.FgMagenta
)
// UpdateTheme updates all theme variables

25
pkg/utils/date.go Normal file
View File

@@ -0,0 +1,25 @@
package utils
import (
"fmt"
"time"
)
func UnixToTimeAgo(timestamp int64) string {
now := time.Now().Unix()
delta := float64(now - timestamp)
// we go seconds, minutes, hours, days, weeks, months, years
conversions := []float64{60, 60, 24, 7, 4.34524, 12}
labels := []string{"s", "m", "h", "d", "w", "m", "y"}
for i, conversion := range conversions {
if delta < conversion {
return fmt.Sprintf("%d%s", int(delta), labels[i])
}
delta /= conversion
}
return fmt.Sprintf("%dy", int(delta))
}
func UnixToDate(timestamp int64) string {
return time.Unix(timestamp, 0).Format(time.RFC822)
}

View File

@@ -39,8 +39,8 @@ func WithPadding(str string, padding int) string {
// ColoredString takes a string and a colour attribute and returns a colored
// string with that attribute
func ColoredString(str string, colorAttribute color.Attribute) string {
colour := color.New(colorAttribute)
func ColoredString(str string, colorAttributes ...color.Attribute) string {
colour := color.New(colorAttributes...)
return ColoredStringDirect(str, colour)
}
@@ -316,3 +316,9 @@ func TruncateWithEllipsis(str string, limit int) string {
remainingLength := limit - len(ellipsis)
return str[0:remainingLength] + "..."
}
func FindStringSubmatch(str string, regexpStr string) (bool, []string) {
re := regexp.MustCompile(regexpStr)
match := re.FindStringSubmatch(str)
return len(match) > 0, match
}

14
vendor/github.com/creack/pty/Dockerfile.riscv generated vendored Normal file
View File

@@ -0,0 +1,14 @@
FROM golang:1.13
# Clone and complie a riscv compatible version of the go compiler.
RUN git clone https://review.gerrithub.io/riscv/riscv-go /riscv-go
# riscvdev branch HEAD as of 2019-06-29.
RUN cd /riscv-go && git checkout 04885fddd096d09d4450726064d06dd107e374bf
ENV PATH=/riscv-go/misc/riscv:/riscv-go/bin:$PATH
RUN cd /riscv-go/src && GOROOT_BOOTSTRAP=$(go env GOROOT) ./make.bash
ENV GOROOT=/riscv-go
# Make sure we compile.
WORKDIR pty
ADD . .
RUN GOOS=linux GOARCH=riscv go build

View File

@@ -4,7 +4,7 @@ Pty is a Go package for using unix pseudo-terminals.
## Install
go get github.com/kr/pty
go get github.com/creack/pty
## Example
@@ -14,7 +14,7 @@ Pty is a Go package for using unix pseudo-terminals.
package main
import (
"github.com/kr/pty"
"github.com/creack/pty"
"io"
"os"
"os/exec"
@@ -50,7 +50,7 @@ import (
"os/signal"
"syscall"
"github.com/kr/pty"
"github.com/creack/pty"
"golang.org/x/crypto/ssh/terminal"
)

4
vendor/github.com/creack/pty/go.mod generated vendored Normal file
View File

@@ -0,0 +1,4 @@
module github.com/creack/pty
go 1.13

View File

@@ -1,4 +1,4 @@
// +build !windows
// +build !windows,!solaris
package pty

30
vendor/github.com/creack/pty/ioctl_solaris.go generated vendored Normal file
View File

@@ -0,0 +1,30 @@
package pty
import (
"golang.org/x/sys/unix"
"unsafe"
)
const (
// see /usr/include/sys/stropts.h
I_PUSH = uintptr((int32('S')<<8 | 002))
I_STR = uintptr((int32('S')<<8 | 010))
I_FIND = uintptr((int32('S')<<8 | 013))
// see /usr/include/sys/ptms.h
ISPTM = (int32('P') << 8) | 1
UNLKPT = (int32('P') << 8) | 2
PTSSTTY = (int32('P') << 8) | 3
ZONEPT = (int32('P') << 8) | 4
OWNERPT = (int32('P') << 8) | 5
)
type strioctl struct {
ic_cmd int32
ic_timout int32
ic_len int32
ic_dp unsafe.Pointer
}
func ioctl(fd, cmd, ptr uintptr) error {
return unix.IoctlSetInt(int(fd), uint(cmd), int(ptr))
}

View File

@@ -46,6 +46,6 @@ func ptsname(f *os.File) (string, error) {
func unlockpt(f *os.File) error {
var u _C_int
// use TIOCSPTLCK with a zero valued arg to clear the slave pty lock
// use TIOCSPTLCK with a pointer to zero to clear the lock
return ioctl(f.Fd(), syscall.TIOCSPTLCK, uintptr(unsafe.Pointer(&u)))
}

View File

@@ -11,7 +11,7 @@ func open() (pty, tty *os.File, err error) {
* from ptm(4):
* The PTMGET command allocates a free pseudo terminal, changes its
* ownership to the caller, revokes the access privileges for all previous
* users, opens the file descriptors for the master and slave devices and
* users, opens the file descriptors for the pty and tty devices and
* returns them to the caller in struct ptmget.
*/

139
vendor/github.com/creack/pty/pty_solaris.go generated vendored Normal file
View File

@@ -0,0 +1,139 @@
package pty
/* based on:
http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libc/port/gen/pt.c
*/
import (
"errors"
"golang.org/x/sys/unix"
"os"
"strconv"
"syscall"
"unsafe"
)
const NODEV = ^uint64(0)
func open() (pty, tty *os.File, err error) {
masterfd, err := syscall.Open("/dev/ptmx", syscall.O_RDWR|unix.O_NOCTTY, 0)
//masterfd, err := syscall.Open("/dev/ptmx", syscall.O_RDWR|syscall.O_CLOEXEC|unix.O_NOCTTY, 0)
if err != nil {
return nil, nil, err
}
p := os.NewFile(uintptr(masterfd), "/dev/ptmx")
sname, err := ptsname(p)
if err != nil {
return nil, nil, err
}
err = grantpt(p)
if err != nil {
return nil, nil, err
}
err = unlockpt(p)
if err != nil {
return nil, nil, err
}
slavefd, err := syscall.Open(sname, os.O_RDWR|unix.O_NOCTTY, 0)
if err != nil {
return nil, nil, err
}
t := os.NewFile(uintptr(slavefd), sname)
// pushing terminal driver STREAMS modules as per pts(7)
for _, mod := range([]string{"ptem", "ldterm", "ttcompat"}) {
err = streams_push(t, mod)
if err != nil {
return nil, nil, err
}
}
return p, t, nil
}
func minor(x uint64) uint64 {
return x & 0377
}
func ptsdev(fd uintptr) uint64 {
istr := strioctl{ISPTM, 0, 0, nil}
err := ioctl(fd, I_STR, uintptr(unsafe.Pointer(&istr)))
if err != nil {
return NODEV
}
var status unix.Stat_t
err = unix.Fstat(int(fd), &status)
if err != nil {
return NODEV
}
return uint64(minor(status.Rdev))
}
func ptsname(f *os.File) (string, error) {
dev := ptsdev(f.Fd())
if dev == NODEV {
return "", errors.New("not a master pty")
}
fn := "/dev/pts/" + strconv.FormatInt(int64(dev), 10)
// access(2) creates the slave device (if the pty exists)
// F_OK == 0 (unistd.h)
err := unix.Access(fn, 0)
if err != nil {
return "", err
}
return fn, nil
}
type pt_own struct {
pto_ruid int32
pto_rgid int32
}
func grantpt(f *os.File) error {
if ptsdev(f.Fd()) == NODEV {
return errors.New("not a master pty")
}
var pto pt_own
pto.pto_ruid = int32(os.Getuid())
// XXX should first attempt to get gid of DEFAULT_TTY_GROUP="tty"
pto.pto_rgid = int32(os.Getgid())
var istr strioctl
istr.ic_cmd = OWNERPT
istr.ic_timout = 0
istr.ic_len = int32(unsafe.Sizeof(istr))
istr.ic_dp = unsafe.Pointer(&pto)
err := ioctl(f.Fd(), I_STR, uintptr(unsafe.Pointer(&istr)))
if err != nil {
return errors.New("access denied")
}
return nil
}
func unlockpt(f *os.File) error {
istr := strioctl{UNLKPT, 0, 0, nil}
return ioctl(f.Fd(), I_STR, uintptr(unsafe.Pointer(&istr)))
}
// push STREAMS modules if not already done so
func streams_push(f *os.File, mod string) error {
var err error
buf := []byte(mod)
// XXX I_FIND is not returning an error when the module
// is already pushed even though truss reports a return
// value of 1. A bug in the Go Solaris syscall interface?
// XXX without this we are at risk of the issue
// https://www.illumos.org/issues/9042
// but since we are not using libc or XPG4.2, we should not be
// double-pushing modules
err = ioctl(f.Fd(), I_FIND, uintptr(unsafe.Pointer(&buf[0])))
if err != nil {
return nil
}
err = ioctl(f.Fd(), I_PUSH, uintptr(unsafe.Pointer(&buf[0])))
return err
}

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