Compare commits

..

1 Commits
blah ... v0.15

Author SHA1 Message Date
Jesse Duffield
91de559f15 add half and fullscreen modes 2020-02-25 08:41:53 +11:00
1323 changed files with 182510 additions and 58564 deletions

62
.circleci/config.yml Normal file
View File

@@ -0,0 +1,62 @@
version: 2
jobs:
build:
docker:
- image: circleci/golang:1.13
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.13
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: /.*/

1
.github/FUNDING.yml vendored
View File

@@ -1,4 +1,5 @@
# These are supported funding model platforms
github: [jesseduffield]
ko_fi: jesseduffield
custom: ['https://donorbox.org/lazygit']

View File

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

View File

@@ -1,45 +0,0 @@
name: Continuous Delivery
on:
push:
tags:
- 'v*'
jobs:
cd:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Unshallow repo
run: git fetch --prune --unshallow
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.14.x
- name: Run goreleaser
uses: goreleaser/goreleaser-action@v1
env:
GITHUB_TOKEN: ${{secrets.GITHUB_API_TOKEN}}
- name: Bump Homebrew
uses: dawidd6/action-homebrew-bump-formula@v3
with:
token: ${{secrets.GITHUB_API_TOKEN}}
formula: lazygit
- name: Checkout PPA repo
uses: actions/checkout@v2
with:
repository: dawidd6/lazygit-debian
token: ${{secrets.GITHUB_API_TOKEN}}
fetch-depth: 0
- name: Update PPA repo
run: |
version="$(echo "$GITHUB_REF" | sed 's@refs/tags/v@@')"
sudo apt install -y git-buildpackage
git fetch --tags https://github.com/$GITHUB_REPOSITORY
gbp import-ref -u "$version"
gbp dch -D xenial -N "$version"-1
git add debian/changelog
git commit -S -m "d/changelog: dch $version"
gbp tag
git push --tags origin master

View File

@@ -1,40 +0,0 @@
name: Continuous Integration
on:
push:
branches:
- master
pull_request:
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

7
.gitignore vendored
View File

@@ -25,9 +25,4 @@ lazygit
!.circleci/
!.github/
test/git_server/data
test/integration/*/actual/
test/integration/*/used_config/
# these sample hooks waste too space space
test/integration/*/expected/.git_keep/hooks/
!.git_keep/
test/git_server/data

View File

@@ -40,8 +40,8 @@ changelog:
- '^bump'
brews:
-
# Repository to push the tap to.
tap:
# Reporitory to push the tap to.
github:
owner: jesseduffield
name: homebrew-lazygit
@@ -61,3 +61,5 @@ brews:
# conflicts:
# - svn
# - bash
# test comment to see if goreleaser only releases on new commits

View File

@@ -1,17 +1,15 @@
# run with:
# docker build -t lazygit .
# docker run -it lazygit:latest /bin/sh
# docker run -it lazygit:latest /bin/sh -l
FROM golang:1.14-alpine3.11
FROM golang:1.13-alpine3.10
WORKDIR /go/src/github.com/jesseduffield/lazygit/
COPY ./ .
RUN CGO_ENABLED=0 GOOS=linux go build
FROM alpine:3.11
FROM alpine:3.10
RUN apk add -U git xdg-utils
WORKDIR /go/src/github.com/jesseduffield/lazygit/
COPY --from=0 /go/src/github.com/jesseduffield/lazygit /go/src/github.com/jesseduffield/lazygit
COPY --from=0 /go/src/github.com/jesseduffield/lazygit/lazygit /bin/
RUN echo "alias gg=lazygit" >> ~/.profile
ENTRYPOINT [ "lazygit" ]

168
README.md
View File

@@ -1,65 +1,32 @@
<p align="center">
<img src="https://i.imgur.com/oYB7Cj8.png">
</p>
# 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)]()
![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)
A simple terminal UI for git commands, written in Go with the [gocui](https://github.com/jroimartin/gocui 'gocui') library.
A simple terminal UI for git commands, written in Go with the [gocui](https://github.com/jroimartin/gocui "gocui") library.
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!*
Rant time: You've heard it before, git is _powerful_, but what good is that power when everything is so damn hard to do? Interactive rebasing requires you to edit a goddamn TODO file in your editor? *Are you kidding me?* To stage part of a file you need to use a command line program stepping through each hunk and if a hunk can't be split down any further but contains code you don't want to stage, bad luck? *Are you KIDDING me?!* Sometimes you get asked to stash your changes when switching branches only to realise that after you switch and unstash that there weren't even any conflicts and it would have been fine to just checkout the branch directly? *YOU HAVE GOT TO BE KIDDING ME!*
If you're a mere mortal like me and you're tired of hearing how powerful git is when in your daily life it's a powerful pain in your ass, lazygit might be for you.
![Gif](../assets/staging.gif)
![Gif](/docs/resources/lazygit-example.gif)
## Table of contents
- [Installation](#installation)
- [Binary releases](#binary-releases)
- [Homebrew](#homebrew)
- [MacPorts](#macports)
- [Ubuntu](#ubuntu)
- [Void Linux](#void-linux)
- [Scoop (Windows)](#scoop-windows)
- [Arch Linux](#arch-linux)
- [Fedora and CentOS 7](#fedora-and-centos-7)
- [Solus Linux](#solus-linux)
- [FreeBSD](#freebsd)
- [Conda](#conda)
- [Go](#go)
- [Manual](#manual)
- [Usage](#usage)
- [Keybindings](#keybindings)
- [Changing directory on exit](#changing-directory-on-exit)
- [Undo/Redo](#undoredo)
- [Configuration](#configuration)
- [Custom pagers](#configuration)
- [Custom commands](#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)
- [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)
[<img src="https://i.imgur.com/sVEktDn.png">](https://youtu.be/CPLdltN7wgE)
Github Sponsors is matching all donations dollar-for-dollar for 12 months so if you're feeling generous consider [sponsoring me](https://github.com/sponsors/jesseduffield)
## Installation
### Binary Releases
For Windows, Mac OS or Linux, you can download a binary release [here](../../releases).
### Homebrew
Normally the lazygit formula can be found in the Homebrew core but we suggest you tap our formula to get the frequently updated one. It works with Linux, too.
Tap:
```
brew install jesseduffield/lazygit/lazygit
```
@@ -71,10 +38,8 @@ brew install lazygit
```
### MacPorts
Latest version built from github releases.
Tap:
```
sudo port install lazygit
```
@@ -99,18 +64,6 @@ They follow upstream latest releases
sudo xbps-install -S lazygit
```
### Scoop (Windows)
You can install `lazygit` using [scoop](https://scoop.sh/). It's in the `extras` bucket:
```sh
# Add the extras bucket
scoop bucket add extras
# Install lazygit
scoop install lazygit
```
### Arch Linux
Packages for Arch Linux are available via AUR (Arch User Repository).
@@ -118,11 +71,11 @@ Packages for Arch Linux are available via AUR (Arch User Repository).
There are two packages. The stable one which is built with the latest release
and the git version which builds from the most recent commit.
- Stable: <https://aur.archlinux.org/packages/lazygit/>
- Development: <https://aur.archlinux.org/packages/lazygit-git/>
- Stable: https://aur.archlinux.org/packages/lazygit/
- Development: https://aur.archlinux.org/packages/lazygit-git/
Instruction of how to install AUR content can be found here:
<https://wiki.archlinux.org/index.php/Arch_User_Repository>
https://wiki.archlinux.org/index.php/Arch_User_Repository
### Fedora and CentOS 7
@@ -133,27 +86,18 @@ sudo dnf copr enable atim/lazygit -y
sudo dnf install lazygit
```
### Solus Linux
```sh
sudo eopkg install lazygit
```
### FreeBSD
```sh
pkg install lazygit
```
### Conda
Released versions are available for different platforms, see <https://anaconda.org/conda-forge/lazygit>
Released versions are available for different platforms, see https://anaconda.org/conda-forge/lazygit
```sh
conda install -c conda-forge lazygit
```
### Binary Release (Windows/Linux/OSX)
You can download a binary release [here](https://github.com/jesseduffield/lazygit/releases).
### Go
```sh
@@ -166,38 +110,20 @@ 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).
### Manual
You'll need to [install Go](https://golang.org/doc/install)
```
git clone https://github.com/jesseduffield/lazygit.git
cd lazygit
go install
```
You can also use `go run main.go` to compile and run in one go (pun definitely intended)
## Usage
Call `lazygit` in your terminal inside a git repository.
```sh
$ lazygit
```
If you want, you can
Call `lazygit` in your terminal inside a git repository. If you want, you can
also add an alias for this with `echo "alias lg='lazygit'" >> ~/.zshrc` (or
whichever rc file you're using).
### Keybindings
You can check out the list of keybindings [here](/docs/keybindings).
### Changing Directory On Exit
- Basic video tutorial [here](https://youtu.be/VDXvbHZYeKY).
- Rebase Magic tutorial [here](https://youtu.be/4XaToVut_hs)
- List of keybindings
[here](/docs/keybindings).
## Changing Directory On Exit
If you change repos in lazygit and want your shell to change directory into that repo on exiting lazygit, add this to your `~/.zshrc` (or other rc file):
```
lg()
{
@@ -211,34 +137,8 @@ lg()
fi
}
```
Then `source ~/.zshrc` and from now on when you call `lg` and exit you'll switch directories to whatever you were in inside lazyigt. To override this behaviour you can exit using `shift+Q` rather than just `q`.
### Undo/Redo
See the [docs](/docs/Undoing.md)
## Configuration
Check out the [configuration docs](docs/Config.md).
### Custom Pagers
See the [docs](docs/Custom_Pagers.md)
### Custom Commands
If lazygit is missing a feature, there's a good chance you can implement it yourself with a custom command!
See the [docs](docs/Custom_Command_Keybindings.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
@@ -250,21 +150,18 @@ See the [docs](docs/Custom_Command_Keybindings.md)
### Resolving merge conflicts
![Gif](../assets/resolving-merge-conflicts.gif)
![Gif](/docs/resources/resolving-merge-conflicts.gif)
### Interactive Rebasing
![Interactive Rebasing](../assets/rebase.gif)
![Interactive Rebasing](/docs/resources/interactive-rebase.png)
## 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](../assets/slack_rgb.png)](https://join.slack.com/t/lazygit/shared_invite/zt-5bo2clzo-hB8ZTVN5dWUCqj5QFiQVLA)
### Debugging Locally
Run `lazygit --debug` in one terminal tab and `lazygit --logs` in another to view the program and its log output side by side
[![Slack](/docs/resources/slack_rgb.png)](https://join.slack.com/t/lazygit/shared_invite/enQtNDE3MjIwNTYyMDA0LTM3Yjk3NzdiYzhhNTA1YjM4Y2M4MWNmNDBkOTI0YTE4YjQ1ZmI2YWRhZTgwNjg2YzhhYjg3NDBlMmQyMTI5N2M)
## Donate
@@ -287,5 +184,4 @@ If you want to see what I (Jesse) am up to in terms of development, follow me on
If you find that lazygit doesn't quite satisfy your requirements, these may be a better fit:
- [GitUI](https://github.com/Extrawurst/gitui)
- [tig](https://github.com/jonas/tig)

View File

@@ -1,63 +1,43 @@
# User Config
# User Config:
Default path for the config file:
* Linux: `~/.config/jesseduffield/lazygit/config.yml`
* MacOS: `~/Library/Application Support/jesseduffield/lazygit/config.yml`
* Windows: `%APPDATA%\jesseduffield\lazygit\config.yml`
## Default
## Default:
```yaml
gui:
# stuff relating to the UI
scrollHeight: 2 # how many lines you scroll by
scrollPastBottom: true # enable scrolling past the bottom
sidePanelWidth: 0.3333 # number from 0 to 1
expandFocusedSidePanel: false
mainPanelSplitMode: 'flexible' # one of 'horizontal' | 'flexible' | 'vertical'
theme:
lightTheme: false # For terminals with a light background
activeBorderColor:
- white
- bold
inactiveBorderColor:
- green
- white
optionsTextColor:
- blue
selectedLineBgColor:
- default
selectedRangeBgColor:
- blue
commitLength:
show: true
mouseEvents: true
skipUnstageLineWarning: false
skipStashWarning: true
git:
paging:
colorArg: always
useConfig: false
merging:
# only applicable to unix users
manualCommit: false
# extra args passed to `git merge`, e.g. --no-ff
args: ""
pull:
mode: 'merge' # one of 'merge' | 'rebase' | 'ff-only'
skipHookPrefix: WIP
autoFetch: true
branchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --"
overrideGpg: false # prevents lazygit from spawning a separate process when using GPG
disableForcePushing: false
update:
method: prompt # can be: prompt | background | never
days: 14 # how often an update is checked for
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
confirmOnQuit: false
# determines whether hitting 'esc' will quit the application when there is nothing to cancel/close
quitOnTopLevelReturn: true
disableStartupPopups: false
keybinding:
universal:
quit: 'q'
@@ -69,22 +49,14 @@ 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
nextBlock-alt: 'l' # goto the next block / panel
nextMatch: 'n'
prevMatch: 'N'
optionMenu: 'x' # show help menu
optionMenu-alt1: '?' # show help menu
select: '<space>'
goInto: '<enter>'
confirm: '<enter>'
confirm-alt1: 'y'
remove: 'd'
new: 'n'
edit: 'e'
@@ -105,14 +77,6 @@ Default path for the config file:
prevTab: '['
nextScreenMode: '+'
prevScreenMode: '_'
undo: 'z'
redo: '<c-z>'
filteringMenu: '<c-s>'
diffingMenu: 'W'
diffingMenu-alt: '<c-e>' # deprecated
copyToClipboard: '<c-o>'
submitEditorText: '<enter>'
appendNewline: '<tab>'
status:
checkForUpdate: 'u'
recentRepos: '<enter>'
@@ -156,9 +120,8 @@ Default path for the config file:
cherryPickCopyRange: 'C'
pasteCommits: 'v'
tagCommit: 'T'
toggleDiffCommit: 'i'
checkoutCommit: '<space>'
resetCherryPick: '<c-R>'
copyCommitMessageToClipboard: '<c-y>'
stash:
popStash: 'g'
commitFiles:
@@ -168,45 +131,42 @@ Default path for the config file:
toggleDragSelect-alt: 'V'
toggleSelectHunk: 'a'
pickBothHunks: 'b'
submodules:
init: 'i'
update: 'u'
bulkMenu: 'b'
undo: 'z'
```
## Platform Defaults
## Platform Defaults:
### Windows
### Windows:
```yaml
os:
openCommand: 'cmd /c "start "" {{filename}}"'
```
### Linux
### Linux:
```yaml
os:
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'
```
### OSX
### OSX:
```yaml
os:
openCommand: 'open {{filename}}'
```
### Recommended Config Values
### Recommended Config Values:
for users of VSCode
```yaml
os:
openCommand: 'code -rg {{filename}}'
openCommand: 'code -r {{filename}}'
```
## Color Attributes
## Color Attributes:
For color attributes you can choose an array of attributes (with max one color attribute)
The available attributes are:
@@ -224,7 +184,7 @@ The available attributes are:
- reverse # useful for high-contrast
- underline
## Light terminal theme
## Light terminal theme:
If you have issues with a light terminal theme where you can't read / see the text add these settings
@@ -238,32 +198,18 @@ If you have issues with a light terminal theme where you can't read / see the te
inactiveBorderColor:
- black
selectedLineBgColor:
- default
- blue
```
## Struggling to see selected line
## Example Coloring:
If you struggle to see the selected line I recomment using the reverse attribute on selected lines like so:
![border example](/docs/resources/colored-border-example.png)
```yaml
gui:
theme:
selectedLineBgColor:
- reverse
selectedRangeBgColor:
- reverse
```
## Keybindings:
For all possible keybinding options, check [Custom_Keybinding.md](https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybinding.md)
## Example Coloring
![border example](../../assets/colored-border-example.png)
## Keybindings
For all possible keybinding options, check [Custom_Keybindings.md](https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md)
### Example Keybindings For Colemak Users
#### Example Keybindings For Colemak Users:
```yaml
keybinding:
universal:
@@ -271,8 +217,6 @@ For all possible keybinding options, check [Custom_Keybindings.md](https://githu
nextItem-alt: 'e'
prevBlock-alt: 'n'
nextBlock-alt: 'i'
nextMatch: '='
prevMatch: '-'
new: 'k'
edit: 'o'
openFile: 'O'
@@ -280,62 +224,13 @@ For all possible keybinding options, check [Custom_Keybindings.md](https://githu
scrollDownMain-alt1: 'E'
scrollUpMain-alt2: '<c-u>'
scrollDownMain-alt2: '<c-e>'
undo: 'l'
redo: '<c-r>'
diffingMenu: 'M'
filteringMenu: '<c-f>'
files:
ignoreFile: 'I'
commits:
moveDownCommit: '<c-e>'
moveUpCommit: '<c-u>'
toggleDiffCommit: 'l'
branches:
viewGitFlowOptions: 'I'
setUpstream: 'U'
```
## Custom pull request URLs
Some git provider setups (e.g. on-premises GitLab) can have distinct URLs for git-related calls and
the web interface/API itself. To work with those, Lazygit needs to know where it needs to create
the pull request. You can do so on your `config.yml` file using the following syntax:
```yaml
services:
"<gitDomain>": "<provider>:<webDomain>"
```
Where:
- `gitDomain` stands for the domain used by git itself (i.e. the one present on clone URLs), e.g. `git.work.com`
- `provider` is one of `github`, `bitbucket` or `gitlab`
- `webDomain` is the URL where your git service exposes a web interface and APIs, e.g. `gitservice.work.com`
## Predefined commit message prefix
In situations where certain naming pattern is used for branches and commits, pattern can be used to populate
commit message with prefix that is parsed from the branch name.
Example:
* Branch name: feature/AB-123
* Commit message: [AB-123] Adding feature
```yaml
git:
commitPrefixes:
my_project: # This is repository folder name
pattern: "^\\w+\\/(\\w+-\\w+)"
replace: "[$1] "
```
## Custom git log command
You can override the `git log` command that's used to render the log of the selected branch like so:
```
git:
branchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium --oneline {{branchName}} --"
```
Result:
![](https://i.imgur.com/Nibq35B.png)

View File

@@ -1,138 +0,0 @@
# Custom Command Keybindings
You can add custom command keybindings in your config.yml (accessible by pressing 'o' on the status panel from within lazygit) like so:
```
customCommands:
- key: '<c-r>'
command: 'hub browse -- "commit/{{.SelectedLocalCommit.Sha}}"'
context: 'commits'
- key: 'a'
command: "git {{if .SelectedFile.HasUnstagedChanges}} add {{else}} reset {{end}} {{.SelectedFile.Name}}"
context: 'files'
description: 'toggle file staged'
- key: 'C'
command: "git commit"
context: 'global'
subprocess: true
- key: 'n'
prompts:
- type: 'menu'
title: 'What kind of branch is it?'
options:
- name: 'feature'
description: 'a feature branch'
value: 'feature'
- name: 'hotfix'
description: 'a hotfix branch'
value: 'hotfix'
- name: 'release'
description: 'a release branch'
value: 'release'
- type: 'input'
title: 'What is the new branch name?'
initialValue: ''
command: "git flow {{index .PromptResponses 0}} start {{index .PromptResponses 1}}"
context: 'localBranches'
loadingText: 'creating branch'
```
Looking at the command assigned to the 'n' key, here's what the result looks like:
![](../../assets/custom-command-keybindings.gif)
Custom command keybindings will appear alongside inbuilt keybindings when you view the options menu by pressing 'x':
![](https://i.imgur.com/QB21FPx.png)
For a given custom command, here are the allowed fields:
| _field_ | _description_ | required |
|-----------------|----------------------|-|
| key | the key to trigger the command. Use a single letter or one of the values from [here](https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md) | yes |
| command | the command to run | yes |
| context | the context in which to listen for the key (see below) | yes |
| subprocess | whether you want the command to run in a subprocess (necessary if you want to view the output of the command or provide user input) | no |
| prompts | a list of prompts that will request user input before running the final command | no |
| loadingText | text to display while waiting for command to finish | no |
| description | text to display in the keybindings menu that appears when you press 'x' | no |
### Contexts
The permitted contexts are:
| _context_ | _description_ |
| -------------- | -------------------------------------------------------------------------------------------------------- |
| status | the 'Status' tab |
| files | the 'Files' tab |
| localBranches | the 'Local Branches' tab |
| remotes | the 'Remotes' tab |
| remoteBranches | the context you get when pressing enter on a remote in the remotes tab |
| tags | the 'Tags' tab |
| commits | the 'Commits' tab |
| reflogCommits | the 'Reflog' tab |
| subCommits | the context you see when pressing enter on a branch |
| commitFiles | the context you see when pressing enter on a commit or stash entry (warning, might be renamed in future) |
| stash | the 'Stash' tab |
| global | this keybinding will take affect everywhere |
### Prompts
The permitted prompt fields are:
| _field_ | _description_ | _required_ |
| ------------ | -------------------------------------------------------------------------------- | ---------- |
| type | one of 'input' or 'menu' | yes |
| title | the title to display in the popup panel | no |
| initialValue | (only applicable to 'input' prompts) the initial value to appear in the text box | no |
| options | (only applicable to 'menu' prompts) the options to display in the menu | no |
The permitted option fields are:
| _field_ | _description_ | _required_ |
|-----------------|----------------------|-|
| name | the string which will appear first on the line | no |
| description | the string which will appear second on the line | no |
| value | the value that will be stored in `.PromptResponses` if the option is selected | yes |
If an option has no name the value will be displayed to the user in place of the name, so you're allowed to only include the value like so:
```
prompts:
- type: 'menu'
title: 'What kind of branch is it?'
options:
- value: 'feature'
- value: 'hotfix'
- value: 'release'
```
### Placeholder values
Your commands can contain placeholder strings using Go's [template syntax](https://jan.newmarch.name/go/template/chapter-template.html). The template syntax is pretty powerful, letting you do things like conditionals if you want, but for the most part you'll simply want to be accessing the fields on the following objects:
```
SelectedLocalCommit
SelectedReflogCommit
SelectedSubCommit
SelectedFile
SelectedLocalBranch
SelectedRemoteBranch
SelectedRemote
SelectedTag
SelectedStashEntry
SelectedCommitFile
CheckedOutBranch
```
To see what fields are available on e.g. the `SelectedFile`, see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/commands/file.go) (all the modelling lives in the same directory). Note that the custom commands feature does not guarantee backwards compatibility (until we hit lazygit version 1.0 of course) which means a field you're accessing on an object may no longer be available from one release to the next. Typically however, all you'll need is `{{.SelectedFile.Name}}`, `{{.SelectedLocalCommit.Sha}}` and `{{.SelectedBranch.Name}}`. In the future we will likely introduce a tighter interface that exposes a limited set of fields for each model.
### Keybinding collisions
If your custom keybinding collides with an inbuilt keybinding that is defined for the same context, only the custom keybinding will be executed. This also applies to the global context. However, one caveat is that if you have a custom keybinding defined on the global context for some key, and there is an in-built keybinding defined for the same key and for a specific context (say the 'files' context), then the in-built keybinding will take precedence. See how to change in-built keybindings [here](https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#keybindings)
### Debugging
If you want to verify that your command actually does what you expect, you can wrap it in an 'echo' call and set `subprocess: true` so that it doesn't actually execute the command but you can see how the placeholders were resolved. Alternatively you can run lazygit in debug mode with `lazygit --debug` and in another terminal window run `lazygit --logs` to see which commands are actually run
### More Examples
See the [wiki](https://github.com/jesseduffield/lazygit/wiki/Custom-Commands-Compendium) page for more examples, and feel free to add your own custom commands to this page so others can benefit!

View File

@@ -1,64 +0,0 @@
# Custom Pagers
Lazygit supports custom pagers, [configured](/docs/Config.md) in the config.yml file (which can be opened by pressing `o` in the Status panel).
Support does not extend to Windows users, because we're making use of a package which doesn't have Windows support.
## Default:
```yaml
git:
paging:
colorArg: always
useConfig: false
```
the `colorArg` key is for whether you want the `--color=always` arg in your `git diff` command. Some pagers want it set to `always`, others want it set to `never`.
## Delta:
```yaml
git:
paging:
colorArg: always
pager: delta --dark --paging=never --24-bit-color=never
```
![](https://i.imgur.com/QJpQkF3.png)
## Diff-so-fancy
```yaml
git:
paging:
colorArg: always
pager: diff-so-fancy
```
![](https://i.imgur.com/rjH1TpT.png)
## ydiff
```yaml
gui:
sidePanelWidth: 0.2 # gives you more space to show things side-by-side
git:
paging:
colorArg: never
pager: ydiff -p cat -s --wrap --width={{columnWidth}}
```
![](https://i.imgur.com/vaa8z0H.png)
Be careful with this one, I think the homebrew and pip versions are behind master. I needed to directly download the ydiff script to get the no-pager functionality working.
## Using git config
```yaml
git:
paging:
colorArg: always
useConfig: true
```
If you set `useConfig: true`, lazygit will use whatever pager is specified in `$GIT_PAGER`, `$PAGER`, or your *git config*. If the pager ends with something like ` | less` we will strip that part out, because less doesn't play nice with our rendering approach. If the custom pager uses less under the hood, that will also break rendering (hence the `--paging=never` flag for the `delta` pager).

View File

@@ -1,88 +0,0 @@
# How To Make And Run Integration Tests For lazygit
Integration tests are located in `test/integration`. Each test will run a bash script to prepare a test repo, then replay a recorded lazygit session from within that repo, and then the resultant repo will be compared to an expected repo that was created upon the initial recording. Each integration test lives in its own directory, and the name of the directory becomes the name of the test. Within the directory must be the following files:
### `test.json`
An example of a `test.json` is:
```
{ "description": "stage a file and commit the change", "speed": 20 }
```
The `speed` key refers to the playback speed as a multiple of the original recording speed. So 20 means the test will run 20 times faster than the original recording speed. If a test fails for a given speed, it will drop the speed and re-test, until finally attempting the test at the original speed. If you omit the speed, it will default to 10.
### `setup.sh`
This is a bash script containing the instructions for creating the test repo from scratch. For example:
```
#!/bin/sh
cd $1
git init
git config user.email "CI@example.com"
git config user.name "CI"
echo test1 > myfile1
git add .
git commit -am "myfile1"
```
## Running tests
To run all tests
```
go test pkg/gui/gui_test.go
```
To run them in parallel
```
PARALLEL=true go test pkg/gui/gui_test.go
```
To run a single test
```
go test pkg/gui/gui_test.go -run /<test name>
```
To run a test at a certain speed
```
SPEED=2 go test pkg/gui/gui_test.go -run /<test name>
```
To update a snapshot
```
UPDATE_SNAPSHOTS=true go test pkg/gui/gui_test.go -run /<test name>
```
## Creating a new test
To create a new test:
1) Copy and paste an existing test directory and rename the new directory to whatever you want the test name to be. Update the test.json file's description to describe your test.
2) Update the `setup.sh` any way you like
3) If you want to have a config folder for just that test, create a `config` directory to contain a `config.yml` and optionally a `state.yml` file. Otherwise, the `test/default_test_config` directory will be used.
4) From the lazygit root directory, run:
```
RECORD_EVENTS=true go test pkg/gui/gui_test.go -run /<test name>
```
5) Feel free to re-attempt recording as many times as you like. In the absence of a proper testing framework, the more deliberate your keypresses, the better!
6) Once satisfied with the recording, stage all the newly created files: `test.json`, `setup.sh`, `recording.json` and the `expected` directory that contains a copy of the repo you created.
The resulting directory will look like:
```
actual/ (the resulting repo after running the test, ignored by git)
expected/ (the 'snapshot' repo)
config/ (need not be present)
test.json
setup.sh
recording.json
```
Feel free to create a hierarchy of directories in the `test/integration` directory to group tests by feature.
## Feedback
If you think this process can be improved, let me know! It shouldn't be too hard to change things.

View File

@@ -1,24 +0,0 @@
# Undo/Redo in lazygit
![Gif](../../assets/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 contain enough 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

@@ -1,39 +1,47 @@
# Lazygit Keybindings
# Lazygit menu
## Global Keybindings
## Global
<pre>
<kbd>pgup</kbd>: scroll up main panel (fn+up)
<kbd>pgdown</kbd>: scroll down main panel (fn+down)
<kbd>m</kbd>: view merge/rebase options
<kbd>ctrl+p</kbd>: view custom patch options
<kbd>P</kbd>: push
<kbd>p</kbd>: pull
<kbd>R</kbd>: refresh
<kbd>x</kbd>: open menu
<kbd>z</kbd>: undo (via reflog) (experimental)
<kbd>ctrl+z</kbd>: redo (via reflog) (experimental)
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode
<kbd>:</kbd>: execute custom command
<kbd>|</kbd>: view scoping options
<kbd>W</kbd>: open diff menu
<kbd>ctrl+e</kbd>: open diff menu
</pre>
## List Panel Navigation
## Status
<pre>
<kbd>]</kbd>: next tab
<kbd>[</kbd>: previous tab
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
<kbd>e</kbd>: edit config file
<kbd>o</kbd>: open config file
<kbd>u</kbd>: check for update
<kbd>s</kbd>: switch to a recent repo
</pre>
## Branches Panel (Branches Tab)
## Files
<pre>
<kbd>c</kbd>: commit changes
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>A</kbd>: amend last commit
<kbd>C</kbd>: commit changes using git editor
<kbd>space</kbd>: toggle staged
<kbd>d</kbd>: view 'discard changes' options
<kbd>e</kbd>: edit file
<kbd>o</kbd>: open file
<kbd>i</kbd>: add to .gitignore
<kbd>r</kbd>: refresh files
<kbd>S</kbd>: stash files
<kbd>a</kbd>: stage/unstage all
<kbd>t</kbd>: add patch
<kbd>D</kbd>: view reset options
<kbd>enter</kbd>: stage individual hunks/lines
<kbd>f</kbd>: fetch
<kbd>X</kbd>: execute custom command
</pre>
## Branches
<pre>
<kbd>space</kbd>: checkout
@@ -42,76 +50,12 @@
<kbd>F</kbd>: force checkout
<kbd>n</kbd>: new branch
<kbd>d</kbd>: delete branch
<kbd>r</kbd>: rebase checked-out branch onto this branch
<kbd>r</kbd>: rebase branch
<kbd>M</kbd>: merge into currently checked out branch
<kbd>i</kbd>: show git-flow options
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>g</kbd>: view reset options
<kbd>R</kbd>: rename branch
<kbd>ctrl+o</kbd>: copy branch name to clipboard
<kbd>enter</kbd>: view commits
</pre>
## Branches Panel (Remote Branches (in Remotes tab))
<pre>
<kbd>esc</kbd>: Return to remotes list
<kbd>g</kbd>: view reset options
<kbd>enter</kbd>: view commits
<kbd>space</kbd>: checkout
<kbd>n</kbd>: new branch
<kbd>M</kbd>: merge into currently checked out branch
<kbd>d</kbd>: delete branch
<kbd>r</kbd>: rebase checked-out branch onto this branch
<kbd>u</kbd>: set as upstream of checked-out branch
</pre>
## Branches Panel (Remotes Tab)
<pre>
<kbd>f</kbd>: fetch remote
<kbd>n</kbd>: add new remote
<kbd>d</kbd>: remove remote
<kbd>e</kbd>: edit remote
</pre>
## Branches Panel (Sub-commits)
<pre>
<kbd>enter</kbd>: view commit's files
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: view reset options
<kbd>n</kbd>: new branch
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
</pre>
## Branches Panel (Tags Tab)
<pre>
<kbd>space</kbd>: checkout
<kbd>d</kbd>: delete tag
<kbd>P</kbd>: push tag
<kbd>n</kbd>: create tag
<kbd>g</kbd>: view reset options
<kbd>enter</kbd>: view commits
</pre>
## Commit Files Panel
<pre>
<kbd>ctrl+o</kbd>: copy the committed file name to the clipboard
<kbd>c</kbd>: checkout file
<kbd>d</kbd>: discard this commit's changes to this file
<kbd>o</kbd>: open file
<kbd>e</kbd>: edit file
<kbd>space</kbd>: toggle file included in patch
<kbd>enter</kbd>: enter file to add selected lines to the patch
</pre>
## Commits Panel (Commits)
## Commits
<pre>
<kbd>s</kbd>: squash down
@@ -129,65 +73,49 @@
<kbd>p</kbd>: pick commit (when mid-rebase)
<kbd>t</kbd>: revert commit
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>v</kbd>: paste commits (cherry-pick)
<kbd>enter</kbd>: view commit's files
<kbd>space</kbd>: checkout commit
<kbd>n</kbd>: create new branch off of commit
<kbd>T</kbd>: tag commit
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>space</kbd>: select commit to diff with another commit
</pre>
## Commits Panel (Reflog Tab)
## Stash
<pre>
<kbd>enter</kbd>: view commit's files
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: view reset options
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
<kbd>space</kbd>: apply
<kbd>g</kbd>: pop
<kbd>d</kbd>: drop
</pre>
## Files Panel (Files)
## Commit files
<pre>
<kbd>c</kbd>: commit changes
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>A</kbd>: amend last commit
<kbd>C</kbd>: commit changes using git editor
<kbd>space</kbd>: toggle staged
<kbd>d</kbd>: view 'discard changes' options
<kbd>e</kbd>: edit file
<kbd>esc</kbd>: go back
<kbd>c</kbd>: checkout file
<kbd>d</kbd>: discard this commit's changes to this file
<kbd>o</kbd>: open file
<kbd>i</kbd>: add to .gitignore
<kbd>r</kbd>: refresh files
<kbd>s</kbd>: stash changes
<kbd>S</kbd>: view stash options
<kbd>a</kbd>: stage/unstage all
<kbd>D</kbd>: view reset options
<kbd>enter</kbd>: stage individual hunks/lines
<kbd>f</kbd>: fetch
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
<kbd>g</kbd>: view upstream reset options
</pre>
## Files Panel (Submodules)
## Main (Normal)
<pre>
<kbd>ctrl+o</kbd>: copy submodule name to clipboard
<kbd>enter</kbd>: enter submodule
<kbd>d</kbd>: view reset and remove submodule options
<kbd>u</kbd>: update submodule
<kbd>n</kbd>: add new submodule
<kbd>e</kbd>: update submodule URL
<kbd>i</kbd>: initialize submodule
<kbd>b</kbd>: view bulk submodule options
<kbd>PgDn</kbd>: scroll down (fn+up)
<kbd>PgUp</kbd>: scroll up (fn+down)
</pre>
## Main Panel (Merging)
## Main (Staging)
<pre>
<kbd>esc</kbd>: return to files panel
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>space</kbd>: stage line
<kbd>a</kbd>: stage hunk
</pre>
## Main (Merging)
<pre>
<kbd>esc</kbd>: return to files panel
@@ -199,72 +127,3 @@
<kbd>▼</kbd>: select bottom hunk
<kbd>z</kbd>: undo
</pre>
## Main Panel (Normal)
<pre>
<kbd> ̄</kbd>: scroll down (fn+up)
<kbd>¦</kbd>: scroll up (fn+down)
</pre>
## Main Panel (Patch Building)
<pre>
<kbd>esc</kbd>: exit line-by-line mode
<kbd>o</kbd>: open file
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>space</kbd>: add/remove line(s) to patch
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
</pre>
## Main Panel (Staging)
<pre>
<kbd>esc</kbd>: return to files panel
<kbd>space</kbd>: toggle line staged / unstaged
<kbd>d</kbd>: delete change (git reset)
<kbd>tab</kbd>: switch to other panel
<kbd>o</kbd>: open file
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>e</kbd>: edit file
<kbd>o</kbd>: open file
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
<kbd>c</kbd>: commit changes
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>C</kbd>: commit changes using git editor
</pre>
## Menu Panel
<pre>
<kbd>esc</kbd>: close menu
</pre>
## Stash Panel
<pre>
<kbd>enter</kbd>: view stash entry's files
<kbd>space</kbd>: apply
<kbd>g</kbd>: pop
<kbd>d</kbd>: drop
<kbd>n</kbd>: new branch
</pre>
## Status Panel
<pre>
<kbd>e</kbd>: edit config file
<kbd>o</kbd>: open config file
<kbd>u</kbd>: check for update
<kbd>enter</kbd>: switch to a recent repo
</pre>

View File

@@ -1,157 +1,24 @@
# Lazygit Sneltoetsen
# Lazygit menu
## Globaale Sneltoetsen
## Global
<pre>
<kbd>pgup</kbd>: scroll naar beneden vanaf hooft paneel (fn+up)
<kbd>pgdown</kbd>: scroll naar beneden vabaf hooft paneel (fn+down)
<kbd>m</kbd>: bekijk merge/rebase opties
<kbd>ctrl+p</kbd>: bekijk aangepaste patch opties
<kbd>P</kbd>: push
<kbd>p</kbd>: pull
<kbd>R</kbd>: verversen
<kbd>x</kbd>: open menu
<kbd>z</kbd>: ongedaan maken (via reflog) (experimenteel)
<kbd>ctrl+z</kbd>: redo (via reflog) (experimenteel)
<kbd>+</kbd>: volgende schermmode (normaal/half/groot )
<kbd>_</kbd>: vorige schermmode
<kbd>:</kbd>: voor aangepast commando uit
<kbd>|</kbd>: bekijk scoping opties
<kbd>W</kbd>: open diff menu
<kbd>ctrl+e</kbd>: open diff menu
</pre>
## List Panel Navigation
## Status
<pre>
<kbd>]</kbd>: volgende tab
<kbd>[</kbd>: vorige tab
<kbd>,</kbd>: vorige pagina
<kbd>.</kbd>: volgende pagina
<kbd><</kbd>: scroll naar boven
<kbd>/</kbd>: start met zoekken
<kbd>></kbd>: scroll naar beneden
<kbd>e</kbd>: verander config file
<kbd>o</kbd>: open config file
<kbd>u</kbd>: check voor updates
<kbd>s</kbd>: wissel naar een recente repo
</pre>
## Branches Paneel (Branches Tab)
<pre>
<kbd>space</kbd>: uitchecken
<kbd>o</kbd>: maak een pull-aanvraag
<kbd>c</kbd>: uitchecken bij naam
<kbd>F</kbd>: forceer checkout
<kbd>n</kbd>: nieuwe branch
<kbd>d</kbd>: verwijder branch
<kbd>r</kbd>: rebase branch
<kbd>M</kbd>: merge in met huidige checked out branch
<kbd>i</kbd>: laat git-flow opties zien
<kbd>f</kbd>: fast-forward deze branch vanaf zijn upstream
<kbd>g</kbd>: bekijk reset opties
<kbd>R</kbd>: hernoem branch
<kbd>ctrl+o</kbd>: copieer branch name naar clipboard
<kbd>enter</kbd>: view commits
</pre>
## Branches Paneel (Remote Branches (in Remotes tab))
<pre>
<kbd>esc</kbd>: Ga terug naar remotes lijst
<kbd>g</kbd>: bekijk reset opties
<kbd>enter</kbd>: view commits
<kbd>space</kbd>: uitchecken
<kbd>n</kbd>: nieuwe branch
<kbd>M</kbd>: merge in met huidige checked out branch
<kbd>d</kbd>: verwijder branch
<kbd>r</kbd>: rebase branch
<kbd>u</kbd>: stel in als upstream van uitgecheckte branch
</pre>
## Branches Paneel (Remotes Tab)
<pre>
<kbd>f</kbd>: fetch remote
<kbd>n</kbd>: voeg een nieuwe remote toe
<kbd>d</kbd>: verwijder remote
<kbd>e</kbd>: wijzig remote
</pre>
## Branches Paneel (Sub-commits)
<pre>
<kbd>enter</kbd>: bekijk gecommite bestanden
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: bekijk reset opties
<kbd>n</kbd>: nieuwe branch
<kbd>c</kbd>: kopiëer commit (cherry-pick)
<kbd>C</kbd>: kopiëer commit reeks (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (gecopieerde) commits selectie
<kbd>ctrl+o</kbd>: copieer commit SHA naar clipboard
</pre>
## Branches Paneel (Tags Tab)
<pre>
<kbd>space</kbd>: uitchecken
<kbd>d</kbd>: verwijder tag
<kbd>P</kbd>: push tag
<kbd>n</kbd>: creëer tag
<kbd>g</kbd>: bekijk reset opties
<kbd>enter</kbd>: view commits
</pre>
## Commit bestanden Paneel
<pre>
<kbd>ctrl+o</kbd>: kopieer de vastgelegde bestandsnaam naar het klembord
<kbd>c</kbd>: bestand uitchecken
<kbd>d</kbd>: uitsluit deze commit zijn veranderingen aan dit bestand
<kbd>o</kbd>: open bestand
<kbd>e</kbd>: verander bestand
<kbd>space</kbd>: toggle bestand inbegrepen in patch
<kbd>enter</kbd>: enter bestand to add selecteered lines to the patch
</pre>
## Commits Paneel (Commits)
<pre>
<kbd>s</kbd>: squash beneden
<kbd>r</kbd>: hernoem commit
<kbd>R</kbd>: hernoem commit met editor
<kbd>g</kbd>: reset naar deze commit
<kbd>f</kbd>: Fixup commit
<kbd>F</kbd>: creëer fixup commit voor deze commit
<kbd>S</kbd>: squash bovenstaande commits
<kbd>d</kbd>: verwijder commit
<kbd>ctrl+j</kbd>: verplaats commit 1 naar beneden
<kbd>ctrl+k</kbd>: verplaats commit 1 naar boven
<kbd>e</kbd>: wijzig commit
<kbd>A</kbd>: wijzig commit met staged veranderingen
<kbd>p</kbd>: kies commit (wanneer midden in rebase)
<kbd>t</kbd>: commit ongedaan maken
<kbd>c</kbd>: kopiëer commit (cherry-pick)
<kbd>ctrl+o</kbd>: copieer commit SHA naar clipboard
<kbd>C</kbd>: kopiëer commit reeks (cherry-pick)
<kbd>v</kbd>: plak commits (cherry-pick)
<kbd>enter</kbd>: bekijk gecommite bestanden
<kbd>space</kbd>: checkout commit
<kbd>n</kbd>: create new branch off of commit
<kbd>T</kbd>: tag commit
<kbd>ctrl+r</kbd>: reset cherry-picked (gecopieerde) commits selectie
</pre>
## Commits Paneel (Reflog Tab)
<pre>
<kbd>enter</kbd>: bekijk gecommite bestanden
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: bekijk reset opties
<kbd>c</kbd>: kopiëer commit (cherry-pick)
<kbd>C</kbd>: kopiëer commit reeks (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (gecopieerde) commits selectie
<kbd>ctrl+o</kbd>: copieer commit SHA naar clipboard
</pre>
## Bestanden Paneel (Bestanden)
## Bestanden
<pre>
<kbd>c</kbd>: Commit veranderingen
@@ -164,35 +31,88 @@
<kbd>o</kbd>: open bestand
<kbd>i</kbd>: voeg toe aan .gitignore
<kbd>r</kbd>: refresh bestanden
<kbd>s</kbd>: stash-bestanden
<kbd>S</kbd>: bekijk stash opties
<kbd>S</kbd>: stash-bestanden
<kbd>a</kbd>: toggle staged alle
<kbd>t</kbd>: bewerkingen toevoegen
<kbd>D</kbd>: bekijk reset opties
<kbd>enter</kbd>: stage individuele hunks/lijnen
<kbd>f</kbd>: fetch
<kbd>ctrl+o</kbd>: kopieer de bestandsnaam naar het klembord
<kbd>g</kbd>: bekijk upstream reset opties
<kbd>X</kbd>: voor aangepast commando uit
</pre>
## Bestanden Paneel (Submodules)
## Branches
<pre>
<kbd>ctrl+o</kbd>: copy submodule name to clipboard
<kbd>enter</kbd>: enter submodule
<kbd>d</kbd>: view reset and remove submodule options
<kbd>u</kbd>: update submodule
<kbd>n</kbd>: add new submodule
<kbd>e</kbd>: update submodule URL
<kbd>i</kbd>: initialize submodule
<kbd>b</kbd>: view bulk submodule options
<kbd>space</kbd>: uitchecken
<kbd>o</kbd>: maak een pull-aanvraag
<kbd>c</kbd>: uitchecken bij naam
<kbd>F</kbd>: forceer checkout
<kbd>n</kbd>: nieuwe branch
<kbd>d</kbd>: verwijder branch
<kbd>r</kbd>: rebase branch
<kbd>M</kbd>: merge in met huidige checked out branch
<kbd>f</kbd>: fast-forward this branch from its upstream
</pre>
## Hooft Paneel (Merggen)
## Commits
<pre>
<kbd>s</kbd>: squash beneden
<kbd>r</kbd>: hernoem commit
<kbd>R</kbd>: rename commit with editor
<kbd>g</kbd>: reset naar deze commit
<kbd>f</kbd>: Fixup commit
<kbd>F</kbd>: creëer fixup commit voor deze commit
<kbd>S</kbd>: squash bovenstaande commits
<kbd>d</kbd>: verwijder commit
<kbd>ctrl+j</kbd>: verplaats commit 1 omlaag
<kbd>ctrl+k</kbd>: verplaats commit 1 omhoog
<kbd>e</kbd>: verander commit
<kbd>A</kbd>: wijzig commit met staged veranderingen
<kbd>p</kbd>: pick commit (when mid-rebase)
<kbd>t</kbd>: commit omgedaan maken
<kbd>c</kbd>: kopiëer commit (cherry-pick)
<kbd>C</kbd>: kopiëer commit reeks (cherry-pick)
<kbd>v</kbd>: plak commits (cherry-pick)
<kbd>enter</kbd>: bekijk gecommite bestanden
<kbd>space</kbd>: select commit to diff with another commit
</pre>
## Stash
<pre>
<kbd>space</kbd>: toepassen
<kbd>g</kbd>: pop
<kbd>d</kbd>: drop
</pre>
## Commit bestanden
<pre>
<kbd>esc</kbd>: ga terug
<kbd>c</kbd>: bestand uitchecken
<kbd>d</kbd>: uitsluit deze commit zijn veranderingen aan dit bestand
<kbd>o</kbd>: open bestand
</pre>
## Hoofd (Stage Lines/Hunks)
<pre>
<kbd>esc</kbd>: ga terug naar het bestanden paneel
<kbd>space</kbd>: kies hunk
<kbd>b</kbd>: kies bijde hunks
<kbd></kbd>: selecteer de vorige lijn
<kbd></kbd>: selecteer de volgende lijn
<kbd>◄</kbd>: selecteer de vorige hunk
<kbd>►</kbd>: selecteer de volgende hunk
<kbd>space</kbd>: stage lijn
<kbd>a</kbd>: stage hunk
</pre>
## Hoofd (Merging)
<pre>
<kbd>esc</kbd>: ga terug naar het bestanden paneel
<kbd>space</kbd>: pick hunk
<kbd>b</kbd>: pick beide hunks
<kbd>◄</kbd>: selecteer voorgaand conflict
<kbd>►</kbd>: selecteer volgende conflict
<kbd>▲</kbd>: selecteer bovenste hunk
@@ -200,71 +120,9 @@
<kbd>z</kbd>: ongedaan maken
</pre>
## Hooft Paneel (Normaal)
## Hoofd (Normaal)
<pre>
<kbd></kbd>: scroll omlaag (fn+up)
<kbd></kbd>: scroll omhoog (fn+down)
</pre>
## Hooft Paneel (Patch Bouwen)
<pre>
<kbd>esc</kbd>: sluit lijn-bij-lijn mode
<kbd>o</kbd>: open bestand
<kbd>▲</kbd>: selecteer de vorige lijn
<kbd>▼</kbd>: selecteer de volgende lijn
<kbd>◄</kbd>: selecteer de vorige hunk
<kbd>►</kbd>: selecteer de volgende hunk
<kbd>space</kbd>: voeg toe/verwijder lijn(en) in patch
<kbd>v</kbd>: toggle drag selecteer
<kbd>V</kbd>: toggle drag selecteer
<kbd>a</kbd>: toggle selecteer hunk
</pre>
## Hooft Paneel (Staging)
<pre>
<kbd>esc</kbd>: ga terug naar het bestanden paneel
<kbd>space</kbd>: toggle lijnen staged / unstaged
<kbd>d</kbd>: verwijdert change (git reset)
<kbd>tab</kbd>: ga naar een ander paneel
<kbd>o</kbd>: open bestand
<kbd>▲</kbd>: selecteer de vorige lijn
<kbd>▼</kbd>: selecteer de volgende lijn
<kbd>◄</kbd>: selecteer de vorige hunk
<kbd>►</kbd>: selecteer de volgende hunk
<kbd>e</kbd>: verander bestand
<kbd>o</kbd>: open bestand
<kbd>v</kbd>: toggle drag selecteer
<kbd>V</kbd>: toggle drag selecteer
<kbd>a</kbd>: toggle selecteer hunk
<kbd>c</kbd>: Commit veranderingen
<kbd>w</kbd>: commit veranderingen zonder pre-commit hook
<kbd>C</kbd>: commit veranderingen met de git editor
</pre>
## Menu Paneel
<pre>
<kbd>esc</kbd>: sluit menu
</pre>
## Stash Paneel
<pre>
<kbd>enter</kbd>: view stash entry's files
<kbd>space</kbd>: toepassen
<kbd>g</kbd>: pop
<kbd>d</kbd>: laten vallen
<kbd>n</kbd>: nieuwe branch
</pre>
## Status Paneel
<pre>
<kbd>e</kbd>: verander config bestand
<kbd>o</kbd>: open config bestand
<kbd>u</kbd>: check voor updates
<kbd>enter</kbd>: wissel naar een recente repo
<kbd>PgDn</kbd>: scroll omlaag (fn+up)
<kbd>PgUp</kbd>: scroll omhoog (fn+down)
</pre>

View File

@@ -1,39 +1,46 @@
# Lazygit Keybindings
# Lazygit menu
## Globalne
<pre>
<kbd>pgup</kbd>: scroll up main panel (fn+up)
<kbd>pgdown</kbd>: scroll down main panel (fn+down)
<kbd>m</kbd>: view merge/rebase options
<kbd>ctrl+p</kbd>: view custom patch options
<kbd>P</kbd>: push
<kbd>p</kbd>: pull
<kbd>R</kbd>: odśwież
<kbd>x</kbd>: open menu
<kbd>z</kbd>: undo (via reflog) (experimental)
<kbd>ctrl+z</kbd>: redo (via reflog) (experimental)
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
<kbd>_</kbd>: prev screen mode
<kbd>:</kbd>: execute custom command
<kbd>|</kbd>: view scoping options
<kbd>W</kbd>: open diff menu
<kbd>ctrl+e</kbd>: open diff menu
</pre>
## List Panel Navigation
## Status
<pre>
<kbd>]</kbd>: next tab
<kbd>[</kbd>: previous tab
<kbd>,</kbd>: previous page
<kbd>.</kbd>: next page
<kbd><</kbd>: scroll to top
<kbd>/</kbd>: start search
<kbd>></kbd>: scroll to bottom
<kbd>e</kbd>: edytuj plik konfiguracyjny
<kbd>o</kbd>: otwórz plik konfiguracyjny
<kbd>u</kbd>: sprawdź aktualizacje
<kbd>s</kbd>: switch to a recent repo
</pre>
## Gałęzie Panel (Branches Tab)
## Pliki
<pre>
<kbd>c</kbd>: commituj zmiany
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>A</kbd>: zmień ostatnie zatwierdzenie
<kbd>C</kbd>: commituj zmiany używając edytora z gita
<kbd>space</kbd>: przełącz zatwierdzenie
<kbd>d</kbd>: view 'discard changes' options
<kbd>e</kbd>: edytuj plik
<kbd>o</kbd>: otwórz plik
<kbd>i</kbd>: dodaj do .gitignore
<kbd>r</kbd>: odśwież pliki
<kbd>S</kbd>: przechowaj pliki
<kbd>a</kbd>: przełącz wszystkie zatwierdzenia
<kbd>t</kbd>: dodaj łatkę
<kbd>D</kbd>: view reset options
<kbd>enter</kbd>: zatwierdź pojedyncze linie
<kbd>f</kbd>: fetch
<kbd>X</kbd>: execute custom command
</pre>
## Gałęzie
<pre>
<kbd>space</kbd>: przełącz
@@ -44,74 +51,10 @@
<kbd>d</kbd>: usuń gałąź
<kbd>r</kbd>: rebase branch
<kbd>M</kbd>: scal do obecnej gałęzi
<kbd>i</kbd>: show git-flow options
<kbd>f</kbd>: fast-forward this branch from its upstream
<kbd>g</kbd>: view reset options
<kbd>R</kbd>: rename branch
<kbd>ctrl+o</kbd>: copy branch name to clipboard
<kbd>enter</kbd>: view commits
</pre>
## Gałęzie Panel (Remote Branches (in Remotes tab))
<pre>
<kbd>esc</kbd>: return to remotes list
<kbd>g</kbd>: view reset options
<kbd>enter</kbd>: view commits
<kbd>space</kbd>: przełącz
<kbd>n</kbd>: nowa gałąź
<kbd>M</kbd>: scal do obecnej gałęzi
<kbd>d</kbd>: usuń gałąź
<kbd>r</kbd>: rebase branch
<kbd>u</kbd>: set as upstream of checked-out branch
</pre>
## Gałęzie Panel (Remotes Tab)
<pre>
<kbd>f</kbd>: fetch remote
<kbd>n</kbd>: add new remote
<kbd>d</kbd>: remove remote
<kbd>e</kbd>: edit remote
</pre>
## Gałęzie Panel (Sub-commits)
<pre>
<kbd>enter</kbd>: view commit's files
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: view reset options
<kbd>n</kbd>: nowa gałąź
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
</pre>
## Gałęzie Panel (Tags Tab)
<pre>
<kbd>space</kbd>: przełącz
<kbd>d</kbd>: delete tag
<kbd>P</kbd>: push tag
<kbd>n</kbd>: create tag
<kbd>g</kbd>: view reset options
<kbd>enter</kbd>: view commits
</pre>
## Commit files Panel
<pre>
<kbd>ctrl+o</kbd>: copy the committed file name to the clipboard
<kbd>c</kbd>: checkout file
<kbd>d</kbd>: discard this commit's changes to this file
<kbd>o</kbd>: otwórz plik
<kbd>e</kbd>: edytuj plik
<kbd>space</kbd>: toggle file included in patch
<kbd>enter</kbd>: enter file to add selected lines to the patch
</pre>
## Commity Panel (Commity)
## Commity
<pre>
<kbd>s</kbd>: ściśnij w dół
@@ -129,65 +72,49 @@
<kbd>p</kbd>: pick commit (when mid-rebase)
<kbd>t</kbd>: revert commit
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>v</kbd>: paste commits (cherry-pick)
<kbd>enter</kbd>: view commit's files
<kbd>space</kbd>: checkout commit
<kbd>n</kbd>: create new branch off of commit
<kbd>T</kbd>: tag commit
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>space</kbd>: select commit to diff with another commit
</pre>
## Commity Panel (Reflog Tab)
## Schowek
<pre>
<kbd>enter</kbd>: view commit's files
<kbd>space</kbd>: checkout commit
<kbd>g</kbd>: view reset options
<kbd>c</kbd>: copy commit (cherry-pick)
<kbd>C</kbd>: copy commit range (cherry-pick)
<kbd>ctrl+r</kbd>: reset cherry-picked (copied) commits selection
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
<kbd>space</kbd>: zastosuj
<kbd>g</kbd>: wyciągnij
<kbd>d</kbd>: porzuć
</pre>
## Pliki Panel (Pliki)
## Commit files
<pre>
<kbd>c</kbd>: commituj zmiany
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>A</kbd>: zmień ostatnie zatwierdzenie
<kbd>C</kbd>: commituj zmiany używając edytora z gita
<kbd>space</kbd>: przełącz zatwierdzenie
<kbd>d</kbd>: view 'discard changes' options
<kbd>e</kbd>: edytuj plik
<kbd>esc</kbd>: go back
<kbd>c</kbd>: checkout file
<kbd>d</kbd>: discard this commit's changes to this file
<kbd>o</kbd>: otwórz plik
<kbd>i</kbd>: dodaj do .gitignore
<kbd>r</kbd>: odśwież pliki
<kbd>s</kbd>: przechowaj pliki
<kbd>S</kbd>: view stash options
<kbd>a</kbd>: przełącz wszystkie zatwierdzenia
<kbd>D</kbd>: view reset options
<kbd>enter</kbd>: zatwierdź pojedyncze linie
<kbd>f</kbd>: fetch
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
<kbd>g</kbd>: view upstream reset options
</pre>
## Pliki Panel (Submodules)
## Main (Normal)
<pre>
<kbd>ctrl+o</kbd>: copy submodule name to clipboard
<kbd>enter</kbd>: enter submodule
<kbd>d</kbd>: view reset and remove submodule options
<kbd>u</kbd>: update submodule
<kbd>n</kbd>: add new submodule
<kbd>e</kbd>: update submodule URL
<kbd>i</kbd>: initialize submodule
<kbd>b</kbd>: view bulk submodule options
<kbd>PgDn</kbd>: scroll down (fn+up)
<kbd>PgUp</kbd>: scroll up (fn+down)
</pre>
## Main Panel (Merging)
## Main (Zatwierdzanie)
<pre>
<kbd>esc</kbd>: wróć do panelu plików
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>space</kbd>: zatwierdź linię
<kbd>a</kbd>: zatwierdź kawałek
</pre>
## Main (Merging)
<pre>
<kbd>esc</kbd>: wróć do panelu plików
@@ -197,74 +124,5 @@
<kbd>►</kbd>: select next conflict
<kbd>▲</kbd>: select top hunk
<kbd>▼</kbd>: select bottom hunk
<kbd>z</kbd>: cofnij
</pre>
## Main Panel (Normal)
<pre>
<kbd> ̄</kbd>: scroll down (fn+up)
<kbd>¦</kbd>: scroll up (fn+down)
</pre>
## Main Panel (Patch Building)
<pre>
<kbd>esc</kbd>: exit line-by-line mode
<kbd>o</kbd>: otwórz plik
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>space</kbd>: add/remove line(s) to patch
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
</pre>
## Main Panel (Zatwierdzanie)
<pre>
<kbd>esc</kbd>: wróć do panelu plików
<kbd>space</kbd>: toggle line staged / unstaged
<kbd>d</kbd>: delete change (git reset)
<kbd>tab</kbd>: switch to other panel
<kbd>o</kbd>: otwórz plik
<kbd>▲</kbd>: select previous line
<kbd>▼</kbd>: select next line
<kbd>◄</kbd>: select previous hunk
<kbd>►</kbd>: select next hunk
<kbd>e</kbd>: edytuj plik
<kbd>o</kbd>: otwórz plik
<kbd>v</kbd>: toggle drag select
<kbd>V</kbd>: toggle drag select
<kbd>a</kbd>: toggle select hunk
<kbd>c</kbd>: commituj zmiany
<kbd>w</kbd>: commit changes without pre-commit hook
<kbd>C</kbd>: commituj zmiany używając edytora z gita
</pre>
## Menu Panel
<pre>
<kbd>esc</kbd>: close menu
</pre>
## Schowek Panel
<pre>
<kbd>enter</kbd>: view stash entry's files
<kbd>space</kbd>: zastosuj
<kbd>g</kbd>: wyciągnij
<kbd>d</kbd>: porzuć
<kbd>n</kbd>: nowa gałąź
</pre>
## Status Panel
<pre>
<kbd>e</kbd>: edytuj plik konfiguracyjny
<kbd>o</kbd>: otwórz plik konfiguracyjny
<kbd>u</kbd>: sprawdź aktualizacje
<kbd>enter</kbd>: switch to a recent repo
<kbd>z</kbd>: undo
</pre>

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

44
go.mod
View File

@@ -1,40 +1,44 @@
module github.com/jesseduffield/lazygit
go 1.14
go 1.13
require (
github.com/OpenPeeDeeP/xdg v1.0.0
github.com/atotto/clipboard v0.1.2
github.com/aybabtme/humanlog v0.4.1
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
github.com/creack/pty v1.1.11
github.com/fatih/color v1.9.0
github.com/fatih/color v1.7.0
github.com/fsnotify/fsnotify v1.4.7
github.com/go-errors/errors v1.1.1
github.com/go-logfmt/logfmt v0.5.0 // indirect
github.com/go-errors/errors v1.0.1
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/imdario/mergo v0.3.11
github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4
github.com/jesseduffield/gocui v0.3.1-0.20201010224802-8a6768078fd7
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe
github.com/jesseduffield/yaml v2.1.0+incompatible
github.com/jesseduffield/gocui v0.3.1-0.20200224201655-5024a02682ed
github.com/jesseduffield/pty v1.2.1
github.com/jesseduffield/rollrus v0.0.0-20190701125922-dd028cb0bfd7 // indirect
github.com/jesseduffield/termbox-go v0.0.0-20200130214842-1d31d1faa3c9 // indirect
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/mattn/go-colorable v0.1.7 // indirect
github.com/mattn/go-runewidth v0.0.9
github.com/mattn/go-colorable v0.1.4 // indirect
github.com/mattn/go-isatty v0.0.11 // indirect
github.com/mattn/go-runewidth v0.0.8
github.com/mgutz/str v1.2.0
github.com/nicksnyder/go-i18n/v2 v2.0.3
github.com/onsi/ginkgo v1.10.3 // indirect
github.com/onsi/gomega v1.7.1 // indirect
github.com/pelletier/go-toml v1.6.0 // indirect
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0
github.com/sirupsen/logrus v1.4.2
github.com/spf13/afero v1.2.2 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.6.1
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
github.com/stretchr/testify v1.4.0
github.com/tcnksm/go-gitconfig v0.1.2
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 // indirect
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c // indirect
golang.org/x/sys v0.0.0-20201005172224-997123666555 // indirect
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
)
replace github.com/go-git/go-git/v5 => github.com/jesseduffield/go-git/v5 v5.1.1

283
go.sum
View File

@@ -1,181 +1,314 @@
github.com/OpenPeeDeeP/xdg v1.0.0 h1:UDLmNjCGFZZCaVMB74DqYEtXkHxnTxcr4FeJVF9uCn8=
github.com/OpenPeeDeeP/xdg v1.0.0/go.mod h1:tMoSueLQlMf0TCldjrJLNIjAc5qAOIcHt5REi88/Ygo=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY=
github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aybabtme/humanlog v0.4.1 h1:D8d9um55rrthJsP8IGSHBcti9lTb/XknmDAX6Zy8tek=
github.com/aybabtme/humanlog v0.4.1/go.mod h1:B0bnQX4FTSU3oftPMTTPvENCy8LqixLDvYJA9TUCAGo=
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 h1:tuijfIjZyjZaHq9xDUh0tNitwXshJpbLkqMOJv4H3do=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/fatih/color v1.7.1-0.20180516100307-2d684516a886 h1:NAFoy+QgUpERgK3y1xiVh5HcOvSeZHpXTTo5qnvnuK4=
github.com/fatih/color v1.7.1-0.20180516100307-2d684516a886/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-errors/errors v1.1.1 h1:ljK/pL5ltg3qoN+OtN6yCv9HWSfMwxSx90GJCZQxYNg=
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
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.2-0.20200613231340-f56387b50c12 h1:PbKy9zOy4aAKrJ5pibIRpVO2BXnK1Tlcg+caKI7Ox5M=
github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
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-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 h1:zN2lZNZRflqFyxVaTIU61KNKQ9C0055u9CAfpmqUvo4=
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3/go.mod h1:nPpo7qLxd6XL3hWJG/O60sR8ZKfMCiIoNap5GvD12KU=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
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/go-git/v5 v5.1.2-0.20201006095850-341962be15a4 h1:GOQrmaE8i+KEdB8NzAegKYd4tPn/inM0I1uo0NXFerg=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
github.com/jesseduffield/gocui v0.3.1-0.20201010224802-8a6768078fd7 h1:K3MGrjmpPtIhfXmKh/zsIF0CdmNKOkjpIwcUfAa/J2A=
github.com/jesseduffield/gocui v0.3.1-0.20201010224802-8a6768078fd7/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe h1:qsVhCf2RFyyKIUe/+gJblbCpXMUki9rZrHuEctg6M/E=
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4=
github.com/jesseduffield/yaml v2.1.0+incompatible h1:HWQJ1gIv2zHKbDYNp0Jwjlj24K8aqpFHnMCynY1EpmE=
github.com/jesseduffield/yaml v2.1.0+incompatible/go.mod h1:w0xGhOSIJCGYYW+hnFPTutCy5aACpkcwbmORt5axGqk=
github.com/jesseduffield/gocui v0.3.1-0.20191116013947-b13bda319532 h1:V1Lk2rm5/p27NjnlF2ezzkxDaisHNcveMNueSD7RYgs=
github.com/jesseduffield/gocui v0.3.1-0.20191116013947-b13bda319532/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20200112025325-6c933915c351 h1:+sSqd2YotacWt+1MNRN8ZmXnYoiJeblZeptzKiHIyv0=
github.com/jesseduffield/gocui v0.3.1-0.20200112025325-6c933915c351/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20200131125953-f679540a7039 h1:CVhilJ8ZdN7GmAI+fbH9829Cp/8hbK7Lijbd4VaNgo0=
github.com/jesseduffield/gocui v0.3.1-0.20200131125953-f679540a7039/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20200131131454-a319843434ac h1:vp7I0RpFq4L46nFA9QQokzhFgr68LRGtwDO9xfq4F+A=
github.com/jesseduffield/gocui v0.3.1-0.20200131131454-a319843434ac/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20200201013258-57fdcf23edc5 h1:tE0w3tuL/bj1o5VMhjjE0ep6i7Fva+RYjKcMFcniJEY=
github.com/jesseduffield/gocui v0.3.1-0.20200201013258-57fdcf23edc5/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20200223105115-3e1f0f7c3efe h1:UQyebauOcBzbGq32kTXwEyuJaqp3BkI8JoCrGs2jijU=
github.com/jesseduffield/gocui v0.3.1-0.20200223105115-3e1f0f7c3efe/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
github.com/jesseduffield/gocui v0.3.1-0.20200224201655-5024a02682ed h1:glGs+mzPZOl1iHiUsBW3918WeFwqsbQQ/jtLkkQXDi4=
github.com/jesseduffield/gocui v0.3.1-0.20200224201655-5024a02682ed/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/roll v0.0.0-20190629104057-695be2e62b00 h1:+JaOkfBNYQYlGD7dgru8mCwYNEc5tRRI8mThlVANhSM=
github.com/jesseduffield/roll v0.0.0-20190629104057-695be2e62b00/go.mod h1:cWNQljQAWYBp4wchyGfql4q2jRNZXxiE1KhVQgz+JaM=
github.com/jesseduffield/rollrus v0.0.0-20190701125922-dd028cb0bfd7 h1:CRD7bVjlGIiV+M0jlsa+XWpneW0KY0e7Y4z3GWb5S4o=
github.com/jesseduffield/rollrus v0.0.0-20190701125922-dd028cb0bfd7/go.mod h1:VspA3aTkEo0Q7TPCLmX1uHNP+Wb4iSDX09hmTRo1QYc=
github.com/jesseduffield/termbox-go v0.0.0-20190630083001-9dd53af7214e h1:tth7wr6+sfSbdpRWWrwvLYyS56HyIRVfq0Qcl2h28wM=
github.com/jesseduffield/termbox-go v0.0.0-20190630083001-9dd53af7214e/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4=
github.com/jesseduffield/termbox-go v0.0.0-20200130214842-1d31d1faa3c9 h1:iBBk1lhFwjwJw//J2m1yyz9S368GeXQTpMVACTyQMh0=
github.com/jesseduffield/termbox-go v0.0.0-20200130214842-1d31d1faa3c9/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0=
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mgutz/str v1.2.0 h1:4IzWSdIz9qPQWLfKZ0rJcV0jcUDpxvP4JVZ4GXQyvSw=
github.com/mgutz/str v1.2.0/go.mod h1:w1v0ofgLaJdoD0HpQ3fycxKD1WtxpjSo151pK/31q6w=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/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/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/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY=
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0 h1:Xuk8ma/ibJ1fOy4Ee11vHhUFHQNpHhrBneOCNHVXS5w=
github.com/shibukawa/configdir v0.0.0-20170330084843-e180dbdc8da0/go.mod h1:7AwjWCpdPhkSmNAgUv5C7EJ4AbmjEB3r047r3DXWu3Y=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk=
github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/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 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
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=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE=
github.com/urfave/cli v1.20.1-0.20180226030253-8e01ec4cd3e2/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/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/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c h1:dk0ukUIHmGHqASjP0iue2261isepFCC6XRCSd1nHgDw=
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c/go.mod h1:iQL9McJNjoIa5mjH6nYTCTZXUN6RP+XW3eib7Ya3XcI=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/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/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/sys v0.0.0-20170407050850-f3918c30c5c2/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e h1:D5TXcfTk7xF7hvieo4QErS3qqCB4teTffacDWr7CI+0=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/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/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201005172224-997123666555 h1:fihtqzYxy4E31W1yUlyRGveTZT1JIP0bmKaDZ2ceKAw=
golang.org/x/sys v0.0.0-20201005172224-997123666555/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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/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=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

81
main.go
View File

@@ -1,7 +1,6 @@
package main
import (
"bytes"
"fmt"
"log"
"os"
@@ -12,8 +11,6 @@ import (
"github.com/integrii/flaggy"
"github.com/jesseduffield/lazygit/pkg/app"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/env"
yaml "github.com/jesseduffield/yaml"
)
var (
@@ -23,14 +20,16 @@ var (
buildSource = "unknown"
)
func projectPath(path string) string {
gopath := os.Getenv("GOPATH")
return filepath.FromSlash(gopath + "/src/github.com/jesseduffield/lazygit/" + path)
}
func main() {
flaggy.DefaultParser.ShowVersionWithVersionFlag = false
repoPath := ""
flaggy.String(&repoPath, "p", "path", "Path of git repo. (equivalent to --work-tree=<path> --git-dir=<path>/.git/)")
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")
repoPath := "."
flaggy.String(&repoPath, "p", "path", "Path of git repo")
dump := ""
flaggy.AddPositionalValue(&dump, "gitargs", 1, false, "Todo file")
@@ -40,77 +39,25 @@ func main() {
flaggy.Bool(&versionFlag, "v", "version", "Print the current version")
debuggingFlag := false
flaggy.Bool(&debuggingFlag, "d", "debug", "Run in debug mode with logging (see --logs flag below). Use the LOG_LEVEL env var to set the log level (debug/info/warn/error)")
logFlag := false
flaggy.Bool(&logFlag, "l", "logs", "Tail lazygit logs (intended to be used when `lazygit --debug` is called in a separate terminal tab)")
flaggy.Bool(&debuggingFlag, "d", "debug", "Run in debug mode with logging")
configFlag := false
flaggy.Bool(&configFlag, "c", "config", "Print the default config")
configDirFlag := false
flaggy.Bool(&configDirFlag, "cd", "print-config-dir", "Print the config directory")
useConfigDir := ""
flaggy.String(&useConfigDir, "ucd", "use-config-dir", "override default config directory with provided directory")
workTree := ""
flaggy.String(&workTree, "w", "work-tree", "equivalent of the --work-tree git argument")
gitDir := ""
flaggy.String(&gitDir, "g", "git-dir", "equivalent of the --git-dir git argument")
flaggy.Bool(&configFlag, "c", "config", "Print the current default config")
flaggy.Parse()
if repoPath != "" {
if workTree != "" || gitDir != "" {
log.Fatal("--path option is incompatible with the --work-tree and --git-dir options")
}
workTree = repoPath
gitDir = filepath.Join(repoPath, ".git")
}
if useConfigDir != "" {
os.Setenv("CONFIG_DIR", useConfigDir)
}
if workTree != "" {
env.SetGitWorkTreeEnv(workTree)
}
if gitDir != "" {
env.SetGitDirEnv(gitDir)
}
if versionFlag {
fmt.Printf("commit=%s, build date=%s, build source=%s, version=%s, os=%s, arch=%s\n", commit, date, buildSource, version, runtime.GOOS, runtime.GOARCH)
os.Exit(0)
}
if configFlag {
var buf bytes.Buffer
encoder := yaml.NewEncoder(&buf)
err := encoder.Encode(config.GetDefaultConfig())
if err != nil {
log.Fatal(err.Error())
}
fmt.Printf("%s\n", buf.String())
fmt.Printf("%s\n", config.GetDefaultConfig())
os.Exit(0)
}
if configDirFlag {
fmt.Printf("%s\n", config.ConfigDir())
os.Exit(0)
}
if logFlag {
app.TailLogs()
os.Exit(0)
}
if workTree != "" {
if err := os.Chdir(workTree); err != nil {
if repoPath != "." {
if err := os.Chdir(repoPath); err != nil {
log.Fatal(err.Error())
}
}
@@ -120,7 +67,7 @@ func main() {
log.Fatal(err.Error())
}
app, err := app.NewApp(appConfig, filterPath)
app, err := app.NewApp(appConfig)
if err == nil {
err = app.Run()
@@ -134,6 +81,6 @@ func main() {
stackTrace := newErr.ErrorStack()
app.Log.Error(stackTrace)
log.Fatal(fmt.Sprintf("%s\n\n%s", app.Tr.ErrorOccurred, stackTrace))
log.Fatal(fmt.Sprintf("%s\n\n%s", app.Tr.SLocalize("ErrorOccurred"), stackTrace))
}
}

View File

@@ -2,26 +2,19 @@ package app
import (
"bufio"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/aybabtme/humanlog"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/env"
"github.com/jesseduffield/lazygit/pkg/gui"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/updates"
"github.com/shibukawa/configdir"
"github.com/sirupsen/logrus"
)
@@ -31,10 +24,10 @@ type App struct {
Config config.AppConfigurer
Log *logrus.Entry
OSCommand *oscommands.OSCommand
OSCommand *commands.OSCommand
GitCommand *commands.GitCommand
Gui *gui.Gui
Tr *i18n.TranslationSet
Tr *i18n.Localizer
Updater *updates.Updater // may only need this on the Gui
ClientContext string
}
@@ -51,6 +44,12 @@ func newProductionLogger(config config.AppConfigurer) *logrus.Logger {
return log
}
func globalConfigDir() string {
configDirs := configdir.New("jesseduffield", "lazygit")
configDir := configDirs.QueryFolders(configdir.Global)[0]
return configDir.Path
}
func getLogLevel() logrus.Level {
strLevel := os.Getenv("LOG_LEVEL")
level, err := logrus.ParseLevel(strLevel)
@@ -60,19 +59,15 @@ func getLogLevel() logrus.Level {
return level
}
func newDevelopmentLogger(configurer config.AppConfigurer) *logrus.Logger {
logger := logrus.New()
logger.SetLevel(getLogLevel())
logPath, err := config.LogPath()
if err != nil {
log.Fatal(err)
}
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
func newDevelopmentLogger(config config.AppConfigurer) *logrus.Logger {
log := logrus.New()
log.SetLevel(getLogLevel())
file, err := os.OpenFile(filepath.Join(globalConfigDir(), "development.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
panic("unable to log to file") // TODO: don't panic (also, remove this call to the `panic` function)
}
logger.SetOutput(file)
return logger
log.SetOutput(file)
return log
}
func newLogger(config config.AppConfigurer) *logrus.Entry {
@@ -96,14 +91,14 @@ func newLogger(config config.AppConfigurer) *logrus.Entry {
}
// NewApp bootstrap a new application
func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
func NewApp(config config.AppConfigurer) (*App, error) {
app := &App{
closers: []io.Closer{},
Config: config,
}
var err error
app.Log = newLogger(config)
app.Tr = i18n.NewTranslationSet(app.Log)
app.Tr = i18n.NewLocalizer(app.Log)
// if we are being called in 'demon' mode, we can just return here
app.ClientContext = os.Getenv("LAZYGIT_CLIENT_COMMAND")
@@ -111,15 +106,14 @@ func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
return app, nil
}
app.OSCommand = oscommands.NewOSCommand(app.Log, config)
app.OSCommand = commands.NewOSCommand(app.Log, config)
app.Updater, err = updates.NewUpdater(app.Log, config, app.OSCommand, app.Tr)
if err != nil {
return app, err
}
showRecentRepos, err := app.setupRepo()
if err != nil {
if err := app.setupRepo(); err != nil {
return app, err
}
@@ -127,95 +121,29 @@ func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
if err != nil {
return app, err
}
app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config, app.Updater, filterPath, showRecentRepos)
app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config, app.Updater)
if err != nil {
return app, err
}
return app, nil
}
func (app *App) validateGitVersion() error {
output, err := app.OSCommand.RunCommandWithOutput("git --version")
// if we get an error anywhere here we'll show the same status
minVersionError := errors.New(app.Tr.MinGitVersionError)
if err != nil {
return minVersionError
}
if isGitVersionValid(output) {
return nil
}
return minVersionError
}
func isGitVersionValid(versionStr string) bool {
// output should be something like: 'git version 2.23.0 (blah)'
re := regexp.MustCompile(`[^\d]+([\d\.]+)`)
matches := re.FindStringSubmatch(versionStr)
if len(matches) == 0 {
return false
}
gitVersion := matches[1]
majorVersion, err := strconv.Atoi(gitVersion[0:1])
if err != nil {
return false
}
if majorVersion < 2 {
return false
}
return true
}
func (app *App) setupRepo() (bool, error) {
if err := app.validateGitVersion(); err != nil {
return false, err
}
if env.GetGitDirEnv() != "" {
// we've been given the git dir directly. We'll verify this dir when initializing our GitCommand object
return false, nil
}
func (app *App) setupRepo() error {
// if we are not in a git repo, we ask if we want to `git init`
if err := app.OSCommand.RunCommand("git status"); err != nil {
cwd, err := os.Getwd()
if err != nil {
return false, err
if !strings.Contains(err.Error(), "Not a git repository") {
return err
}
info, _ := os.Stat(filepath.Join(cwd, ".git"))
if info != nil && info.IsDir() {
return false, err // Current directory appears to be a git repository.
}
// Offer to initialize a new repository in current directory.
fmt.Print(app.Tr.CreateRepo)
fmt.Print(app.Tr.SLocalize("CreateRepo"))
response, _ := bufio.NewReader(os.Stdin).ReadString('\n')
if strings.Trim(response, " \n") != "y" {
// check if we have a recent repo we can open
recentRepos := app.Config.GetAppState().RecentRepos
if len(recentRepos) > 0 {
var err error
// try opening each repo in turn, in case any have been deleted
for _, repoDir := range recentRepos {
if err = os.Chdir(repoDir); err == nil {
return true, nil
}
}
return false, err
}
os.Exit(1)
}
if err := app.OSCommand.RunCommand("git init"); err != nil {
return false, err
return err
}
}
return false, nil
return nil
}
func (app *App) Run() error {
@@ -231,14 +159,6 @@ func (app *App) Run() error {
return err
}
func gitDir() string {
dir := env.GetGitDirEnv()
if dir == "" {
return ".git"
}
return dir
}
// Rebase contains logic for when we've been run in demon mode, meaning we've
// given lazygit as a command for git to call e.g. to edit a file
func (app *App) Rebase() error {
@@ -250,7 +170,7 @@ func (app *App) Rebase() error {
return err
}
} else if strings.HasSuffix(os.Args[1], filepath.Join(gitDir(), "COMMIT_EDITMSG")) { // TODO: test
} else if strings.HasSuffix(os.Args[1], ".git/COMMIT_EDITMSG") {
// if we are rebasing and squashing, we'll see a COMMIT_EDITMSG
// but in this case we don't need to edit it, so we'll just return
} else {
@@ -275,18 +195,10 @@ func (app *App) Close() error {
func (app *App) KnownError(err error) (string, bool) {
errorMessage := err.Error()
knownErrorMessages := []string{app.Tr.MinGitVersionError}
for _, message := range knownErrorMessages {
if errorMessage == message {
return message, true
}
}
mappings := []errorMapping{
{
originalError: "fatal: not a git repository",
newError: app.Tr.NotARepository,
originalError: "fatal: not a git repository (or any of the parent directories): .git",
newError: app.Tr.SLocalize("notARepository"),
},
}
@@ -297,39 +209,3 @@ func (app *App) KnownError(err error) (string, bool) {
}
return "", false
}
func TailLogs() {
logFilePath, err := config.LogPath()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Tailing log file %s\n\n", logFilePath)
_, err = os.Stat(logFilePath)
if err != nil {
if os.IsNotExist(err) {
log.Fatal("Log file does not exist. Run `lazygit --debug` first to create the log file")
}
log.Fatal(err)
}
cmd := exec.Command("tail", "-f", logFilePath)
stdout, _ := cmd.StdoutPipe()
if err := cmd.Start(); err != nil {
log.Fatal(err)
}
opts := humanlog.DefaultOptions
opts.Truncates = false
if err := humanlog.Scanner(stdout, os.Stdout, opts); err != nil {
log.Fatal(err)
}
if err := cmd.Wait(); err != nil {
log.Fatal(err)
}
os.Exit(0)
}

View File

@@ -1,44 +0,0 @@
package app
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsGitVersionValid(t *testing.T) {
type scenario struct {
versionStr string
expectedResult bool
}
scenarios := []scenario{
{
"",
false,
},
{
"git version 1.9.0",
false,
},
{
"git version 1.9.0 (Apple Git-128)",
false,
},
{
"git version 2.4.0",
true,
},
{
"git version 2.24.3 (Apple Git-128)",
true,
},
}
for _, s := range scenarios {
t.Run(s.versionStr, func(t *testing.T) {
result := isGitVersionValid(s.versionStr)
assert.Equal(t, result, s.expectedResult)
})
}
}

47
pkg/commands/branch.go Normal file
View File

@@ -0,0 +1,47 @@
package commands
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// Branch : A git branch
// duplicating this for now
type Branch struct {
Name string
Recency string
Pushables string
Pullables string
Selected bool
}
// GetDisplayStrings returns the display string of branch
func (b *Branch) GetDisplayStrings(isFocused bool) []string {
displayName := utils.ColoredString(b.Name, GetBranchColor(b.Name))
if isFocused && b.Selected && b.Pushables != "" && b.Pullables != "" {
displayName = fmt.Sprintf("%s ↑%s↓%s", displayName, b.Pushables, b.Pullables)
}
return []string{b.Recency, displayName}
}
// GetBranchColor branch color
func GetBranchColor(name string) color.Attribute {
branchType := strings.Split(name, "/")[0]
switch branchType {
case "feature":
return color.FgGreen
case "bugfix":
return color.FgYellow
case "hotfix":
return color.FgRed
default:
return theme.DefaultTextColor
}
}

View File

@@ -0,0 +1,174 @@
package commands
import (
"regexp"
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
"gopkg.in/src-d/go-git.v4/plumbing"
)
// context:
// we want to only show 'safe' branches (ones that haven't e.g. been deleted)
// which `git branch -a` gives us, but we also want the recency data that
// git reflog gives us.
// So we get the HEAD, then append get the reflog branches that intersect with
// our safe branches, then add the remaining safe branches, ensuring uniqueness
// along the way
// if we find out we need to use one of these functions in the git.go file, we
// can just pull them out of here and put them there and then call them from in here
// BranchListBuilder returns a list of Branch objects for the current repo
type BranchListBuilder struct {
Log *logrus.Entry
GitCommand *GitCommand
}
// NewBranchListBuilder builds a new branch list builder
func NewBranchListBuilder(log *logrus.Entry, gitCommand *GitCommand) (*BranchListBuilder, error) {
return &BranchListBuilder{
Log: log,
GitCommand: gitCommand,
}, nil
}
func (b *BranchListBuilder) obtainCurrentBranch() *Branch {
branchName, err := b.GitCommand.CurrentBranchName()
if err != nil {
panic(err.Error())
}
return &Branch{Name: strings.TrimSpace(branchName)}
}
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
}
branchLines := utils.SplitLines(rawString)
for _, line := range branchLines {
recency, branchName := branchInfoFromLine(line)
if branchName == "" {
continue
}
branch := &Branch{Name: branchName, Recency: recency}
branches = append(branches, branch)
}
return uniqueByName(branches)
}
func (b *BranchListBuilder) obtainSafeBranches() []*Branch {
branches := make([]*Branch, 0)
bIter, err := b.GitCommand.Repo.Branches()
if err != nil {
panic(err)
}
bIter.ForEach(func(b *plumbing.Reference) error {
name := b.Name().Short()
branches = append(branches, &Branch{Name: name})
return nil
})
return branches
}
func (b *BranchListBuilder) appendNewBranches(finalBranches, newBranches, existingBranches []*Branch, included bool) []*Branch {
for _, newBranch := range newBranches {
if included == branchIncluded(newBranch.Name, existingBranches) {
finalBranches = append(finalBranches, newBranch)
}
}
return finalBranches
}
func sanitisedReflogName(reflogBranch *Branch, safeBranches []*Branch) string {
for _, safeBranch := range safeBranches {
if strings.ToLower(safeBranch.Name) == strings.ToLower(reflogBranch.Name) {
return safeBranch.Name
}
}
return reflogBranch.Name
}
// Build the list of branches for the current repo
func (b *BranchListBuilder) Build() []*Branch {
branches := make([]*Branch, 0)
head := b.obtainCurrentBranch()
safeBranches := b.obtainSafeBranches()
reflogBranches := b.obtainReflogBranches()
for i, reflogBranch := range reflogBranches {
reflogBranches[i].Name = sanitisedReflogName(reflogBranch, safeBranches)
}
branches = b.appendNewBranches(branches, reflogBranches, safeBranches, true)
branches = b.appendNewBranches(branches, safeBranches, branches, false)
if len(branches) == 0 || branches[0].Name != head.Name {
branches = append([]*Branch{head}, branches...)
}
branches[0].Recency = " *"
return branches
}
func branchIncluded(branchName string, branches []*Branch) bool {
for _, existingBranch := range branches {
if strings.ToLower(existingBranch.Name) == strings.ToLower(branchName) {
return true
}
}
return false
}
func uniqueByName(branches []*Branch) []*Branch {
finalBranches := make([]*Branch, 0)
for _, branch := range branches {
if branchIncluded(branch.Name, finalBranches) {
continue
}
finalBranches = append(finalBranches, branch)
}
return finalBranches
}
// A line will have the form '10 days ago master' so we need to strip out the
// useful information from that into timeNumber, timeUnit, and branchName
func branchInfoFromLine(line string) (string, string) {
// 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]
}

View File

@@ -1,157 +0,0 @@
package commands
import (
"fmt"
"regexp"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// NewBranch create new branch
func (c *GitCommand) NewBranch(name string, base string) error {
return c.OSCommand.RunCommand("git checkout -b %s %s", name, base)
}
// 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" {
trimmedBranchName := strings.TrimSpace(branchName)
return trimmedBranchName, trimmedBranchName, 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
func (c *GitCommand) DeleteBranch(branch string, force bool) error {
command := "git branch -d"
if force {
command = "git branch -D"
}
return c.OSCommand.RunCommand("%s %s", command, branch)
}
// Checkout checks out a branch (or commit), with --force if you set the force arg to true
type CheckoutOptions struct {
Force bool
EnvVars []string
}
func (c *GitCommand) Checkout(branch string, options CheckoutOptions) error {
forceArg := ""
if options.Force {
forceArg = "--force "
}
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git checkout %s %s", forceArg, branch), oscommands.RunCommandOptions{EnvVars: options.EnvVars})
}
// GetBranchGraph gets the color-formatted graph of the log for the given branch
// Currently it limits the result to 100 commits, but when we get async stuff
// working we can do lazy loading
func (c *GitCommand) GetBranchGraph(branchName string) (string, error) {
cmdStr := c.GetBranchGraphCmdStr(branchName)
return c.OSCommand.RunCommandWithOutput(cmdStr)
}
func (c *GitCommand) GetUpstreamForBranch(branchName string) (string, error) {
output, err := c.OSCommand.RunCommandWithOutput("git rev-parse --abbrev-ref --symbolic-full-name %s@{u}", branchName)
return strings.TrimSpace(output), err
}
func (c *GitCommand) GetBranchGraphCmdStr(branchName string) string {
branchLogCmdTemplate := c.Config.GetUserConfig().Git.BranchLogCmd
templateValues := map[string]string{
"branchName": branchName,
}
return utils.ResolvePlaceholderString(branchLogCmdTemplate, templateValues)
}
func (c *GitCommand) SetUpstreamBranch(upstream string) error {
return c.OSCommand.RunCommand("git branch -u %s", upstream)
}
func (c *GitCommand) SetBranchUpstream(remoteName string, remoteBranchName string, branchName string) error {
return c.OSCommand.RunCommand("git branch --set-upstream-to=%s/%s %s", remoteName, remoteBranchName, branchName)
}
func (c *GitCommand) GetCurrentBranchUpstreamDifferenceCount() (string, string) {
return c.GetCommitDifferences("HEAD", "HEAD@{u}")
}
func (c *GitCommand) GetBranchUpstreamDifferenceCount(branchName string) (string, string) {
return c.GetCommitDifferences(branchName, branchName+"@{u}")
}
// GetCommitDifferences checks how many pushables/pullables there are for the
// current branch
func (c *GitCommand) GetCommitDifferences(from, to string) (string, string) {
command := "git rev-list %s..%s --count"
pushableCount, err := c.OSCommand.RunCommandWithOutput(command, to, from)
if err != nil {
return "?", "?"
}
pullableCount, err := c.OSCommand.RunCommandWithOutput(command, from, to)
if err != nil {
return "?", "?"
}
return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount)
}
type MergeOpts struct {
FastForwardOnly bool
}
// Merge merge
func (c *GitCommand) Merge(branchName string, opts MergeOpts) error {
mergeArgs := c.Config.GetUserConfig().Git.Merging.Args
command := fmt.Sprintf("git merge --no-edit %s %s", mergeArgs, branchName)
if opts.FastForwardOnly {
command = fmt.Sprintf("%s --ff-only", command)
}
return c.OSCommand.RunCommand(command)
}
// AbortMerge abort merge
func (c *GitCommand) AbortMerge() error {
return c.OSCommand.RunCommand("git merge --abort")
}
func (c *GitCommand) IsHeadDetached() bool {
err := c.OSCommand.RunCommand("git symbolic-ref -q HEAD")
return err != nil
}
// ResetHardHead runs `git reset --hard`
func (c *GitCommand) ResetHard(ref string) error {
return c.OSCommand.RunCommand("git reset --hard " + ref)
}
// ResetSoft runs `git reset --soft HEAD`
func (c *GitCommand) ResetSoft(ref string) error {
return c.OSCommand.RunCommand("git reset --soft " + ref)
}
func (c *GitCommand) RenameBranch(oldName string, newName string) error {
return c.OSCommand.RunCommand("git branch --move %s %s", oldName, newName)
}

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

@@ -0,0 +1,69 @@
package commands
import (
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// Commit : A git commit
type Commit struct {
Sha string
Name string
Status string // one of "unpushed", "pushed", "merged", "rebasing" or "selected"
DisplayString string
Action string // one of "", "pick", "edit", "squash", "reword", "drop", "fixup"
Copied bool // to know if this commit is ready to be cherry-picked somewhere
Tags []string
}
// GetDisplayStrings is a function.
func (c *Commit) GetDisplayStrings(isFocused bool) []string {
red := color.New(color.FgRed)
yellow := color.New(color.FgYellow)
green := color.New(color.FgGreen)
blue := color.New(color.FgBlue)
cyan := color.New(color.FgCyan)
defaultColor := color.New(theme.DefaultTextColor)
magenta := color.New(color.FgMagenta)
// for some reason, setting the background to blue pads out the other commits
// horizontally. For the sake of accessibility I'm considering this a feature,
// not a bug
copied := color.New(color.FgCyan, color.BgBlue)
var shaColor *color.Color
switch c.Status {
case "unpushed":
shaColor = red
case "pushed":
shaColor = yellow
case "merged":
shaColor = green
case "rebasing":
shaColor = blue
case "reflog":
shaColor = blue
case "selected":
shaColor = magenta
default:
shaColor = defaultColor
}
if c.Copied {
shaColor = copied
}
actionString := ""
tagString := ""
if c.Action != "" {
actionString = cyan.Sprint(utils.WithPadding(c.Action, 7)) + " "
} else if len(c.Tags) > 0 {
tagColor := color.New(color.FgMagenta, color.Bold)
tagString = utils.ColoredStringDirect(strings.Join(c.Tags, " "), tagColor) + " "
}
return []string{shaColor.Sprint(c.Sha[:8]), actionString + tagString + defaultColor.Sprint(c.Name)}
}

View File

@@ -0,0 +1,42 @@
package commands
import (
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/theme"
)
// CommitFile : A git commit file
type CommitFile struct {
Sha string
Name string
DisplayString string
Status int // one of 'WHOLE' 'PART' 'NONE'
}
const (
// UNSELECTED is for when the commit file has not been added to the patch in any way
UNSELECTED = iota
// WHOLE is for when you want to add the whole diff of a file to the patch,
// including e.g. if it was deleted
WHOLE = iota
// PART is for when you're only talking about specific lines that have been modified
PART
)
// GetDisplayStrings is a function.
func (f *CommitFile) GetDisplayStrings(isFocused bool) []string {
yellow := color.New(color.FgYellow)
green := color.New(color.FgGreen)
defaultColor := color.New(theme.DefaultTextColor)
var colour *color.Color
switch f.Status {
case UNSELECTED:
colour = defaultColor
case WHOLE:
colour = green
case PART:
colour = yellow
}
return []string{colour.Sprint(f.DisplayString)}
}

View File

@@ -0,0 +1,335 @@
package commands
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
// context:
// here we get the commits from git log but format them to show whether they're
// unpushed/pushed/merged into the base branch or not, or if they're yet to
// be processed as part of a rebase (these won't appear in git log but we
// grab them from the rebase-related files in the .git directory to show them
// if we find out we need to use one of these functions in the git.go file, we
// can just pull them out of here and put them there and then call them from in here
// CommitListBuilder returns a list of Branch objects for the current repo
type CommitListBuilder struct {
Log *logrus.Entry
GitCommand *GitCommand
OSCommand *OSCommand
Tr *i18n.Localizer
CherryPickedCommits []*Commit
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) {
return &CommitListBuilder{
Log: log,
GitCommand: gitCommand,
OSCommand: osCommand,
Tr: tr,
CherryPickedCommits: cherryPickedCommits,
DiffEntries: diffEntries,
}, nil
}
// nameAndTag takes a line from a git log and extracts the sha, message and tag (if present)
// example inputs:
// 66e6369c284e96ed5af5 (tag: v0.14.4) allow fastforwarding the current branch
// 32e650e0bb3f4327749f (HEAD -> show-tags) this is my commit
// 32e650e0bb3f4327749e this is my other commit
func (c *CommitListBuilder) commitLineParts(line string) (string, string, []string) {
re := regexp.MustCompile(`(\w+) (.*)`)
shaMatch := re.FindStringSubmatch(line)
if len(shaMatch) <= 1 {
return line, "", nil
}
sha := shaMatch[1]
rest := shaMatch[2]
if !strings.HasPrefix(rest, "(") {
return sha, rest, nil
}
re = regexp.MustCompile(`\((.*)\) (.*)`)
parensMatch := re.FindStringSubmatch(rest)
if len(parensMatch) <= 1 {
return sha, rest, nil
}
notes := parensMatch[1]
message := parensMatch[2]
re = regexp.MustCompile(`tag: ([^,]+)`)
tagMatch := re.FindStringSubmatch(notes)
if len(tagMatch) <= 1 {
return sha, message, nil
}
tag := tagMatch[1]
return sha, message, []string{tag}
}
// GetCommits obtains the commits of the current branch
func (c *CommitListBuilder) GetCommits(limit bool) ([]*Commit, error) {
commits := []*Commit{}
var rebasingCommits []*Commit
rebaseMode, err := c.GitCommand.RebaseMode()
if err != nil {
return nil, err
}
if rebaseMode != "" {
// here we want to also prepend the commits that we're in the process of rebasing
rebasingCommits, err = c.getRebasingCommits(rebaseMode)
if err != nil {
return nil, err
}
if len(rebasingCommits) > 0 {
commits = append(commits, rebasingCommits...)
}
}
unpushedCommits := c.getUnpushedCommits()
log := c.getLog(limit)
// now we can split it up and turn it into commits
for _, line := range utils.SplitLines(log) {
sha, name, tags := c.commitLineParts(line)
_, unpushed := unpushedCommits[sha]
status := map[bool]string{true: "unpushed", false: "pushed"}[unpushed]
commits = append(commits, &Commit{
Sha: sha,
Name: name,
Status: status,
DisplayString: line,
Tags: tags,
})
}
if rebaseMode != "" {
currentCommit := commits[len(rebasingCommits)]
blue := color.New(color.FgYellow)
youAreHere := blue.Sprintf("<-- %s ---", c.Tr.SLocalize("YouAreHere"))
currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name)
}
commits, err = c.setCommitMergedStatuses(commits)
if err != nil {
return nil, err
}
commits, err = c.setCommitCherryPickStatuses(commits)
if err != nil {
return nil, err
}
for _, commit := range commits {
for _, entry := range c.DiffEntries {
if entry.Sha == commit.Sha {
commit.Status = "selected"
}
}
}
return commits, nil
}
// getRebasingCommits obtains the commits that we're in the process of rebasing
func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*Commit, error) {
switch rebaseMode {
case "normal":
return c.getNormalRebasingCommits()
case "interactive":
return c.getInteractiveRebasingCommits()
default:
return nil, nil
}
}
func (c *CommitListBuilder) getNormalRebasingCommits() ([]*Commit, error) {
rewrittenCount := 0
bytesContent, err := ioutil.ReadFile(fmt.Sprintf("%s/rebase-apply/rewritten", c.GitCommand.DotGitDir))
if err == nil {
content := string(bytesContent)
rewrittenCount = len(strings.Split(content, "\n"))
}
// we know we're rebasing, so lets get all the files whose names have numbers
commits := []*Commit{}
err = filepath.Walk(fmt.Sprintf("%s/rebase-apply", c.GitCommand.DotGitDir), func(path string, f os.FileInfo, err error) error {
if rewrittenCount > 0 {
rewrittenCount--
return nil
}
if err != nil {
return err
}
re := regexp.MustCompile(`^\d+$`)
if !re.MatchString(f.Name()) {
return nil
}
bytesContent, err := ioutil.ReadFile(path)
if err != nil {
return err
}
content := string(bytesContent)
commit, err := c.commitFromPatch(content)
if err != nil {
return err
}
commits = append([]*Commit{commit}, commits...)
return nil
})
if err != nil {
return nil, err
}
return commits, nil
}
// git-rebase-todo example:
// pick ac446ae94ee560bdb8d1d057278657b251aaef17 ac446ae
// pick afb893148791a2fbd8091aeb81deba4930c73031 afb8931
// git-rebase-todo.backup example:
// pick 49cbba374296938ea86bbd4bf4fee2f6ba5cccf6 third commit on master
// pick ac446ae94ee560bdb8d1d057278657b251aaef17 blah commit on master
// pick afb893148791a2fbd8091aeb81deba4930c73031 fourth commit on master
// getInteractiveRebasingCommits takes our git-rebase-todo and our git-rebase-todo.backup files
// and extracts out the sha and names of commits that we still have to go
// in the rebase:
func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*Commit, error) {
bytesContent, err := ioutil.ReadFile(fmt.Sprintf("%s/rebase-merge/git-rebase-todo", c.GitCommand.DotGitDir))
if err != nil {
c.Log.Info(fmt.Sprintf("error occurred reading git-rebase-todo: %s", err.Error()))
// we assume an error means the file doesn't exist so we just return
return nil, nil
}
commits := []*Commit{}
lines := strings.Split(string(bytesContent), "\n")
for _, line := range lines {
if line == "" || line == "noop" {
return commits, nil
}
splitLine := strings.Split(line, " ")
commits = append([]*Commit{{
Sha: splitLine[1],
Name: strings.Join(splitLine[2:], " "),
Status: "rebasing",
Action: splitLine[0],
}}, commits...)
}
return nil, nil
}
// assuming the file starts like this:
// From e93d4193e6dd45ca9cf3a5a273d7ba6cd8b8fb20 Mon Sep 17 00:00:00 2001
// From: Lazygit Tester <test@example.com>
// Date: Wed, 5 Dec 2018 21:03:23 +1100
// Subject: second commit on master
func (c *CommitListBuilder) commitFromPatch(content string) (*Commit, error) {
lines := strings.Split(content, "\n")
sha := strings.Split(lines[0], " ")[1]
name := strings.TrimPrefix(lines[3], "Subject: ")
return &Commit{
Sha: sha,
Name: name,
Status: "rebasing",
}, nil
}
func (c *CommitListBuilder) setCommitMergedStatuses(commits []*Commit) ([]*Commit, error) {
ancestor, err := c.getMergeBase()
if err != nil {
return nil, err
}
if ancestor == "" {
return commits, nil
}
passedAncestor := false
for i, commit := range commits {
if strings.HasPrefix(ancestor, commit.Sha) {
passedAncestor = true
}
if commit.Status != "pushed" {
continue
}
if passedAncestor {
commits[i].Status = "merged"
}
}
return commits, nil
}
func (c *CommitListBuilder) 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()
if err != nil {
return "", err
}
baseBranch := "master"
if strings.HasPrefix(currentBranch, "feature/") {
baseBranch = "develop"
}
// swallowing error because it's not a big deal; probably because there are no commits yet
output, _ := c.OSCommand.RunCommandWithOutput("git merge-base HEAD %s", baseBranch)
return output, nil
}
// getUnpushedCommits Returns the sha's of the commits that have not yet been pushed
// to the remote branch of the current branch, a map is returned to ease look up
func (c *CommitListBuilder) getUnpushedCommits() map[string]bool {
pushables := map[string]bool{}
o, err := c.OSCommand.RunCommandWithOutput("git rev-list @{u}..HEAD --abbrev-commit")
if err != nil {
return pushables
}
for _, p := range utils.SplitLines(o) {
pushables[p] = true
}
return pushables
}
// getLog gets the git log.
func (c *CommitListBuilder) getLog(limit bool) string {
limitFlag := ""
if limit {
limitFlag = "-30"
}
result, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git log --decorate --oneline %s --abbrev=%d", limitFlag, 20))
if err != nil {
// assume if there is an error there are no commits yet for this branch
return ""
}
return result
}

View File

@@ -0,0 +1,318 @@
package commands
import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/stretchr/testify/assert"
)
// NewDummyCommitListBuilder creates a new dummy CommitListBuilder for testing
func NewDummyCommitListBuilder() *CommitListBuilder {
osCommand := NewDummyOSCommand()
return &CommitListBuilder{
Log: NewDummyLog(),
GitCommand: NewDummyGitCommandWithOSCommand(osCommand),
OSCommand: osCommand,
Tr: i18n.NewLocalizer(NewDummyLog()),
CherryPickedCommits: []*Commit{},
}
}
// TestCommitListBuilderGetUnpushedCommits is a function.
func TestCommitListBuilderGetUnpushedCommits(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(map[string]bool)
}
scenarios := []scenario{
{
"Can't retrieve pushable commits",
func(string, ...string) *exec.Cmd {
return exec.Command("test")
},
func(pushables map[string]bool) {
assert.EqualValues(t, map[string]bool{}, pushables)
},
},
{
"Retrieve pushable commits",
func(cmd string, args ...string) *exec.Cmd {
return exec.Command("echo", "8a2bb0e\n78976bc")
},
func(pushables map[string]bool) {
assert.Len(t, pushables, 2)
assert.EqualValues(t, map[string]bool{"8a2bb0e": true, "78976bc": true}, pushables)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.getUnpushedCommits())
})
}
}
// TestCommitListBuilderGetMergeBase is a function.
func TestCommitListBuilderGetMergeBase(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(string, error)
}
scenarios := []scenario{
{
"swallows an error if the call to merge-base returns an error",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("test")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "", output)
},
},
{
"returns the commit when master",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("echo", "blah")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.Equal(t, "blah\n", output)
},
},
{
"checks against develop when a feature branch",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "feature/test")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "develop"}, args)
return exec.Command("echo", "blah")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.Equal(t, "blah\n", output)
},
},
{
"bubbles up error if there is one",
func(cmd string, args ...string) *exec.Cmd {
return exec.Command("test")
},
func(output string, err error) {
assert.Error(t, err)
assert.Equal(t, "", output)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.getMergeBase())
})
}
}
// TestCommitListBuilderGetLog is a function.
func TestCommitListBuilderGetLog(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(string)
}
scenarios := []scenario{
{
"Retrieves logs",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"log", "--decorate", "--oneline", "-30", "--abbrev=20"}, args)
return exec.Command("echo", "6f0b32f commands/git : add GetCommits tests refactor\n9d9d775 circle : remove new line")
},
func(output string) {
assert.EqualValues(t, "6f0b32f commands/git : add GetCommits tests refactor\n9d9d775 circle : remove new line\n", output)
},
},
{
"An error occurred when retrieving logs",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"log", "--decorate", "--oneline", "-30", "--abbrev=20"}, args)
return exec.Command("test")
},
func(output string) {
assert.Empty(t, output)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.getLog(true))
})
}
}
// TestCommitListBuilderGetCommits is a function.
func TestCommitListBuilderGetCommits(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func([]*Commit, error)
}
scenarios := []scenario{
{
"No data found",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "rev-list":
assert.EqualValues(t, []string{"rev-list", "@{u}..HEAD", "--abbrev-commit"}, args)
return exec.Command("echo")
case "log":
assert.EqualValues(t, []string{"log", "--decorate", "--oneline", "-30", "--abbrev=20"}, args)
return exec.Command("echo")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("test")
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
}
return nil
},
func(commits []*Commit, err error) {
assert.NoError(t, err)
assert.Len(t, commits, 0)
},
},
{
"GetCommits returns 2 commits, 1 unpushed, the other merged",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "rev-list":
assert.EqualValues(t, []string{"rev-list", "@{u}..HEAD", "--abbrev-commit"}, args)
return exec.Command("echo", "8a2bb0e")
case "log":
assert.EqualValues(t, []string{"log", "--decorate", "--oneline", "-30", "--abbrev=20"}, args)
return exec.Command("echo", "8a2bb0e commit 1\n78976bc commit 2")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("echo", "78976bc")
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
}
return nil
},
func(commits []*Commit, err error) {
assert.NoError(t, err)
assert.Len(t, commits, 2)
assert.EqualValues(t, []*Commit{
{
Sha: "8a2bb0e",
Name: "commit 1",
Status: "unpushed",
DisplayString: "8a2bb0e commit 1",
},
{
Sha: "78976bc",
Name: "commit 2",
Status: "merged",
DisplayString: "78976bc commit 2",
},
}, commits)
},
},
{
"GetCommits bubbles up an error from setCommitMergedStatuses",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "rev-list":
assert.EqualValues(t, []string{"rev-list", "@{u}..HEAD", "--abbrev-commit"}, args)
return exec.Command("echo", "8a2bb0e")
case "log":
assert.EqualValues(t, []string{"log", "--decorate", "--oneline", "-30", "--abbrev=20"}, args)
return exec.Command("echo", "8a2bb0e commit 1\n78976bc commit 2")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("echo", "78976bc")
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
// here's where we are returning the error
return exec.Command("test")
case "branch":
assert.EqualValues(t, []string{"branch", "--contains"}, args)
// here too
return exec.Command("test")
case "rev-parse":
assert.EqualValues(t, []string{"rev-parse", "--short", "HEAD"}, args)
// here too
return exec.Command("test")
}
return nil
},
func(commits []*Commit, err error) {
assert.Error(t, err)
assert.Len(t, commits, 0)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.GetCommits(true))
})
}
}

View File

@@ -1,99 +0,0 @@
package commands
import (
"fmt"
"os/exec"
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
)
// RenameCommit renames the topmost commit with the given name
func (c *GitCommand) RenameCommit(name string) error {
return c.OSCommand.RunCommand("git commit --allow-empty --amend -m %s", c.OSCommand.Quote(name))
}
// ResetToCommit reset to commit
func (c *GitCommand) ResetToCommit(sha string, strength string, options oscommands.RunCommandOptions) error {
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git reset --%s %s", strength, sha), options)
}
// Commit commits to git
func (c *GitCommand) Commit(message string, flags string) (*exec.Cmd, error) {
splitMessage := strings.Split(message, "\n")
lineArgs := ""
for _, line := range splitMessage {
lineArgs += fmt.Sprintf(" -m %s", strconv.Quote(line))
}
command := fmt.Sprintf("git commit %s%s", flags, lineArgs)
if c.usingGpg() {
return c.OSCommand.ShellCommandFromString(command), nil
}
return nil, c.OSCommand.RunCommand(command)
}
// Get the subject of the HEAD commit
func (c *GitCommand) GetHeadCommitMessage() (string, error) {
cmdStr := "git log -1 --pretty=%s"
message, err := c.OSCommand.RunCommandWithOutput(cmdStr)
return strings.TrimSpace(message), err
}
func (c *GitCommand) GetCommitMessage(commitSha string) (string, error) {
cmdStr := "git rev-list --format=%B --max-count=1 " + commitSha
messageWithHeader, err := c.OSCommand.RunCommandWithOutput(cmdStr)
message := strings.Join(strings.SplitAfter(messageWithHeader, "\n")[1:], "\n")
return strings.TrimSpace(message), err
}
// AmendHead amends HEAD with whatever is staged in your working tree
func (c *GitCommand) AmendHead() (*exec.Cmd, error) {
command := "git commit --amend --no-edit --allow-empty"
if c.usingGpg() {
return c.OSCommand.ShellCommandFromString(command), nil
}
return nil, c.OSCommand.RunCommand(command)
}
// PrepareCommitAmendSubProcess prepares a subprocess for `git commit --amend --allow-empty`
func (c *GitCommand) PrepareCommitAmendSubProcess() *exec.Cmd {
return c.OSCommand.PrepareSubProcess("git", "commit", "--amend", "--allow-empty")
}
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 --submodule --color=%s --no-renames --stat -p %s %s", c.colorArg(), sha, filterPathArg)
}
// Revert reverts the selected commit by sha
func (c *GitCommand) Revert(sha string) error {
return c.OSCommand.RunCommand("git revert %s", sha)
}
// CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD
func (c *GitCommand) CherryPickCommits(commits []*models.Commit) error {
todo := ""
for _, commit := range commits {
todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo
}
cmd, err := c.PrepareInteractiveRebaseCommand("HEAD", todo, false)
if err != nil {
return err
}
return c.OSCommand.RunPreparedCommand(cmd)
}
// 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)
}

View File

@@ -1,52 +0,0 @@
package commands
import (
"os"
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (c *GitCommand) ConfiguredPager() string {
if os.Getenv("GIT_PAGER") != "" {
return os.Getenv("GIT_PAGER")
}
if os.Getenv("PAGER") != "" {
return os.Getenv("PAGER")
}
output, err := c.OSCommand.RunCommandWithOutput("git config --get-all core.pager")
if err != nil {
return ""
}
trimmedOutput := strings.TrimSpace(output)
return strings.Split(trimmedOutput, "\n")[0]
}
func (c *GitCommand) GetPager(width int) string {
useConfig := c.Config.GetUserConfig().Git.Paging.UseConfig
if useConfig {
pager := c.ConfiguredPager()
return strings.Split(pager, "| less")[0]
}
templateValues := map[string]string{
"columnWidth": strconv.Itoa(width/2 - 6),
}
pagerTemplate := c.Config.GetUserConfig().Git.Paging.Pager
return utils.ResolvePlaceholderString(pagerTemplate, templateValues)
}
func (c *GitCommand) colorArg() string {
return c.Config.GetUserConfig().Git.Paging.ColorArg
}
func (c *GitCommand) GetConfigValue(key string) string {
output, err := c.OSCommand.RunCommandWithOutput("git config --get %s", key)
if err != nil {
// looks like this returns an error if there is no matching value which we're okay with
return ""
}
return strings.TrimSpace(output)
}

View File

@@ -1,24 +1,56 @@
package commands
import (
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"io/ioutil"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
yaml "gopkg.in/yaml.v2"
)
// This file exports dummy constructors for use by tests in other packages
// NewDummyOSCommand creates a new dummy OSCommand for testing
func NewDummyOSCommand() *OSCommand {
return NewOSCommand(NewDummyLog(), NewDummyAppConfig())
}
// NewDummyAppConfig creates a new dummy AppConfig for testing
func NewDummyAppConfig() *config.AppConfig {
appConfig := &config.AppConfig{
Name: "lazygit",
Version: "unversioned",
Commit: "",
BuildDate: "",
Debug: false,
BuildSource: "",
UserConfig: viper.New(),
}
_ = yaml.Unmarshal([]byte{}, appConfig.AppState)
return appConfig
}
// NewDummyLog creates a new dummy Log for testing
func NewDummyLog() *logrus.Entry {
log := logrus.New()
log.Out = ioutil.Discard
return log.WithField("test", "test")
}
// NewDummyGitCommand creates a new dummy GitCommand for testing
func NewDummyGitCommand() *GitCommand {
return NewDummyGitCommandWithOSCommand(oscommands.NewDummyOSCommand())
return NewDummyGitCommandWithOSCommand(NewDummyOSCommand())
}
// NewDummyGitCommandWithOSCommand creates a new dummy GitCommand for testing
func NewDummyGitCommandWithOSCommand(osCommand *oscommands.OSCommand) *GitCommand {
func NewDummyGitCommandWithOSCommand(osCommand *OSCommand) *GitCommand {
return &GitCommand{
Log: utils.NewDummyLog(),
Log: NewDummyLog(),
OSCommand: osCommand,
Tr: i18n.NewTranslationSet(utils.NewDummyLog()),
Config: config.NewDummyAppConfig(),
Tr: i18n.NewLocalizer(NewDummyLog()),
Config: NewDummyAppConfig(),
getGlobalGitConfig: func(string) (string, error) { return "", nil },
getLocalGitConfig: func(string) (string, error) { return "", nil },
removeFile: func(string) error { return nil },

View File

@@ -1,4 +1,4 @@
package utils
package commands
import "github.com/go-errors/errors"

View File

@@ -1,6 +1,6 @@
// +build !windows
package oscommands
package commands
import (
"bufio"
@@ -9,9 +9,8 @@ import (
"unicode/utf8"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/creack/pty"
"github.com/jesseduffield/pty"
)
// RunCommandWithOutputLiveWrapper runs a command and return every word that gets written in stdout
@@ -19,7 +18,6 @@ import (
// As return of output you need to give a string that will be written to stdin
// NOTE: If the return data is empty it won't written anything to stdin
func RunCommandWithOutputLiveWrapper(c *OSCommand, command string, output func(string) string) error {
c.Log.WithField("command", command).Info("RunCommand")
cmd := c.ExecutableFromString(command)
cmd.Env = append(cmd.Env, "LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8")
@@ -32,14 +30,14 @@ func RunCommandWithOutputLiveWrapper(c *OSCommand, command string, output func(s
return err
}
go utils.Safe(func() {
go func() {
scanner := bufio.NewScanner(ptmx)
scanner.Split(scanWordsWithNewLines)
for scanner.Scan() {
toOutput := strings.Trim(scanner.Text(), " ")
_, _ = ptmx.WriteString(output(toOutput))
}
})
}()
err = cmd.Wait()
ptmx.Close()

View File

@@ -1,6 +1,6 @@
// +build windows
package oscommands
package commands
// RunCommandWithOutputLiveWrapper runs a command live but because of windows compatibility this command can't be ran there
// TODO: Remove this hack and replace it with a proper way to run commands live on windows

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

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

View File

@@ -1,264 +0,0 @@
package commands
import (
"fmt"
"path/filepath"
"strings"
"time"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// CatFile obtains the content of a file
func (c *GitCommand) CatFile(fileName string) (string, error) {
return c.OSCommand.RunCommandWithOutput("%s %s", c.OSCommand.Platform.CatCmd, c.OSCommand.Quote(fileName))
}
// StageFile stages a file
func (c *GitCommand) StageFile(fileName string) error {
// renamed files look like "file1 -> file2"
fileNames := strings.Split(fileName, " -> ")
return c.OSCommand.RunCommand("git add %s", c.OSCommand.Quote(fileNames[len(fileNames)-1]))
}
// StageAll stages all files
func (c *GitCommand) StageAll() error {
return c.OSCommand.RunCommand("git add -A")
}
// UnstageAll stages all files
func (c *GitCommand) UnstageAll() error {
return c.OSCommand.RunCommand("git reset")
}
// UnStageFile unstages a file
func (c *GitCommand) UnStageFile(fileName string, tracked bool) error {
command := "git rm --cached --force %s"
if tracked {
command = "git reset HEAD %s"
}
// renamed files look like "file1 -> file2"
fileNames := strings.Split(fileName, " -> ")
for _, name := range fileNames {
if err := c.OSCommand.RunCommand(command, c.OSCommand.Quote(name)); err != nil {
return err
}
}
return nil
}
func (c *GitCommand) BeforeAndAfterFileForRename(file *models.File) (*models.File, *models.File, error) {
if !file.IsRename() {
return nil, nil, errors.New("Expected renamed file")
}
// we've got a file that represents a rename from one file to another. Unfortunately
// our File abstraction fails to consider this case, so here we will refetch
// all files, passing the --no-renames flag and then recursively call the function
// again for the before file and after file. At some point we should fix the abstraction itself
split := strings.Split(file.Name, " -> ")
filesWithoutRenames := c.GetStatusFiles(GetStatusFileOptions{NoRenames: true})
var beforeFile *models.File
var afterFile *models.File
for _, f := range filesWithoutRenames {
if f.Name == split[0] {
beforeFile = f
}
if f.Name == split[1] {
afterFile = f
}
}
if beforeFile == nil || afterFile == nil {
return nil, nil, errors.New("Could not find deleted file or new file for file rename")
}
if beforeFile.IsRename() || afterFile.IsRename() {
// probably won't happen but we want to ensure we don't get an infinite loop
return nil, nil, errors.New("Nested rename found")
}
return beforeFile, afterFile, nil
}
// DiscardAllFileChanges directly
func (c *GitCommand) DiscardAllFileChanges(file *models.File) error {
if file.IsRename() {
beforeFile, afterFile, err := c.BeforeAndAfterFileForRename(file)
if err != nil {
return err
}
if err := c.DiscardAllFileChanges(beforeFile); err != nil {
return err
}
if err := c.DiscardAllFileChanges(afterFile); err != nil {
return err
}
return nil
}
// if the file isn't tracked, we assume you want to delete it
quotedFileName := c.OSCommand.Quote(file.Name)
if file.HasStagedChanges || file.HasMergeConflicts {
if err := c.OSCommand.RunCommand("git reset -- %s", quotedFileName); err != nil {
return err
}
}
if !file.Tracked {
return c.removeFile(file.Name)
}
return c.DiscardUnstagedFileChanges(file)
}
// DiscardUnstagedFileChanges directly
func (c *GitCommand) DiscardUnstagedFileChanges(file *models.File) error {
quotedFileName := c.OSCommand.Quote(file.Name)
return c.OSCommand.RunCommand("git checkout -- %s", quotedFileName)
}
// Ignore adds a file to the gitignore for the repo
func (c *GitCommand) Ignore(filename string) error {
return c.OSCommand.AppendLineToFile(".gitignore", filename)
}
// WorktreeFileDiff returns the diff of a file
func (c *GitCommand) WorktreeFileDiff(file *models.File, plain bool, cached bool) string {
// for now we assume an error means the file was deleted
s, _ := c.OSCommand.RunCommandWithOutput(c.WorktreeFileDiffCmdStr(file, plain, cached))
return s
}
func (c *GitCommand) WorktreeFileDiffCmdStr(file *models.File, plain bool, cached bool) string {
cachedArg := ""
trackedArg := "--"
colorArg := c.colorArg()
split := strings.Split(file.Name, " -> ") // in case of a renamed file we get the new filename
fileName := c.OSCommand.Quote(split[len(split)-1])
if cached {
cachedArg = "--cached"
}
if !file.Tracked && !file.HasStagedChanges && !cached {
trackedArg = "--no-index /dev/null"
}
if plain {
colorArg = "never"
}
return fmt.Sprintf("git diff --submodule --no-ext-diff --color=%s %s %s %s", colorArg, cachedArg, trackedArg, fileName)
}
func (c *GitCommand) ApplyPatch(patch string, flags ...string) error {
filepath := filepath.Join(c.Config.GetUserConfigDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch")
c.Log.Infof("saving temporary patch to %s", filepath)
if err := c.OSCommand.CreateFileWithContent(filepath, patch); err != nil {
return err
}
flagStr := ""
for _, flag := range flags {
flagStr += " --" + flag
}
return c.OSCommand.RunCommand("git apply %s %s", flagStr, c.OSCommand.Quote(filepath))
}
// ShowFileDiff get the diff of specified from and to. Typically this will be used for a single commit so it'll be 123abc^..123abc
// but when we're in diff mode it could be any 'from' to any 'to'. The reverse flag is also here thanks to diff mode.
func (c *GitCommand) ShowFileDiff(from string, to string, reverse bool, fileName string, plain bool) (string, error) {
cmdStr := c.ShowFileDiffCmdStr(from, to, reverse, fileName, plain)
return c.OSCommand.RunCommandWithOutput(cmdStr)
}
func (c *GitCommand) ShowFileDiffCmdStr(from string, to string, reverse bool, fileName string, plain bool) string {
colorArg := c.colorArg()
if plain {
colorArg = "never"
}
reverseFlag := ""
if reverse {
reverseFlag = " -R "
}
return fmt.Sprintf("git diff --submodule --no-ext-diff --no-renames --color=%s %s %s %s -- %s", colorArg, from, to, reverseFlag, fileName)
}
// CheckoutFile checks out the file for the given commit
func (c *GitCommand) CheckoutFile(commitSha, fileName string) error {
return c.OSCommand.RunCommand("git checkout %s %s", commitSha, fileName)
}
// DiscardOldFileChanges discards changes to a file from an old commit
func (c *GitCommand) DiscardOldFileChanges(commits []*models.Commit, commitIndex int, fileName string) error {
if err := c.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil {
return err
}
// check if file exists in previous commit (this command returns an error if the file doesn't exist)
if err := c.OSCommand.RunCommand("git cat-file -e HEAD^:%s", fileName); err != nil {
if err := c.OSCommand.Remove(fileName); err != nil {
return err
}
if err := c.StageFile(fileName); err != nil {
return err
}
} else if err := c.CheckoutFile("HEAD^", fileName); err != nil {
return err
}
// amend the commit
cmd, err := c.AmendHead()
if cmd != nil {
return errors.New("received unexpected pointer to cmd")
}
if err != nil {
return err
}
// continue
return c.GenericMergeOrRebaseAction("rebase", "continue")
}
// DiscardAnyUnstagedFileChanges discards any unstages file changes via `git checkout -- .`
func (c *GitCommand) DiscardAnyUnstagedFileChanges() error {
return c.OSCommand.RunCommand("git checkout -- .")
}
// RemoveTrackedFiles will delete the given file(s) even if they are currently tracked
func (c *GitCommand) RemoveTrackedFiles(name string) error {
return c.OSCommand.RunCommand("git rm -r --cached %s", name)
}
// RemoveUntrackedFiles runs `git clean -fd`
func (c *GitCommand) RemoveUntrackedFiles() error {
return c.OSCommand.RunCommand("git clean -fd")
}
// ResetAndClean removes all unstaged changes and removes all untracked files
func (c *GitCommand) ResetAndClean() error {
submoduleConfigs, err := c.GetSubmoduleConfigs()
if err != nil {
return err
}
if len(submoduleConfigs) > 0 {
if err := c.ResetSubmodules(submoduleConfigs); err != nil {
return err
}
}
if err := c.ResetHard("HEAD"); err != nil {
return err
}
return c.RemoveUntrackedFiles()
}

File diff suppressed because it is too large Load Diff

View File

@@ -6,19 +6,14 @@ import (
"os"
"os/exec"
"regexp"
"runtime"
"testing"
"time"
"github.com/go-errors/errors"
gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/test"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
gogit "gopkg.in/src-d/go-git.v4"
)
type fileInfoMock struct {
@@ -154,13 +149,13 @@ func TestNavigateToRepoRootDirectory(t *testing.T) {
}
}
// TestSetupRepository is a function.
func TestSetupRepository(t *testing.T) {
// TestSetupRepositoryAndWorktree is a function.
func TestSetupRepositoryAndWorktree(t *testing.T) {
type scenario struct {
testName string
openGitRepository func(string) (*gogit.Repository, error)
errorStr string
test func(*gogit.Repository, error)
sLocalize func(string) string
test func(*gogit.Repository, *gogit.Worktree, error)
}
scenarios := []scenario{
@@ -169,8 +164,10 @@ func TestSetupRepository(t *testing.T) {
func(string) (*gogit.Repository, error) {
return nil, fmt.Errorf(`unquoted '\' must be followed by new line`)
},
"error translated",
func(r *gogit.Repository, err error) {
func(string) string {
return "error translated"
},
func(r *gogit.Repository, w *gogit.Worktree, err error) {
assert.Error(t, err)
assert.EqualError(t, err, "error translated")
},
@@ -180,12 +177,23 @@ func TestSetupRepository(t *testing.T) {
func(string) (*gogit.Repository, error) {
return nil, fmt.Errorf("Error from inside gogit")
},
"",
func(r *gogit.Repository, err error) {
func(string) string { return "" },
func(r *gogit.Repository, w *gogit.Worktree, err error) {
assert.Error(t, err)
assert.EqualError(t, err, "Error from inside gogit")
},
},
{
"An error occurred cause git repository is a bare repository",
func(string) (*gogit.Repository, error) {
return &gogit.Repository{}, nil
},
func(string) string { return "" },
func(r *gogit.Repository, w *gogit.Worktree, err error) {
assert.Error(t, err)
assert.Equal(t, gogit.ErrIsBareRepository, err)
},
},
{
"Setup done properly",
func(string) (*gogit.Repository, error) {
@@ -194,9 +202,10 @@ func TestSetupRepository(t *testing.T) {
assert.NoError(t, err)
return r, nil
},
"",
func(r *gogit.Repository, err error) {
func(string) string { return "" },
func(r *gogit.Repository, w *gogit.Worktree, err error) {
assert.NoError(t, err)
assert.NotNil(t, w)
assert.NotNil(t, r)
},
},
@@ -204,7 +213,7 @@ func TestSetupRepository(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
s.test(setupRepository(s.openGitRepository, s.errorStr))
s.test(setupRepositoryAndWorktree(s.openGitRepository, s.sLocalize))
})
}
}
@@ -252,7 +261,7 @@ func TestNewGitCommand(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
s.setup()
s.test(NewGitCommand(utils.NewDummyLog(), oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog()), config.NewDummyAppConfig()))
s.test(NewGitCommand(NewDummyLog(), NewDummyOSCommand(), i18n.NewLocalizer(NewDummyLog()), NewDummyAppConfig()))
})
}
}
@@ -262,7 +271,7 @@ func TestGitCommandGetStashEntries(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func([]*models.StashEntry)
test func([]*StashEntry)
}
scenarios := []scenario{
@@ -271,7 +280,7 @@ func TestGitCommandGetStashEntries(t *testing.T) {
func(string, ...string) *exec.Cmd {
return exec.Command("echo")
},
func(entries []*models.StashEntry) {
func(entries []*StashEntry) {
assert.Len(t, entries, 0)
},
},
@@ -280,15 +289,17 @@ func TestGitCommandGetStashEntries(t *testing.T) {
func(string, ...string) *exec.Cmd {
return exec.Command("echo", "WIP on add-pkg-commands-test: 55c6af2 increase parallel build\nWIP on master: bb86a3f update github template")
},
func(entries []*models.StashEntry) {
expected := []*models.StashEntry{
func(entries []*StashEntry) {
expected := []*StashEntry{
{
Index: 0,
Name: "WIP on add-pkg-commands-test: 55c6af2 increase parallel build",
0,
"WIP on add-pkg-commands-test: 55c6af2 increase parallel build",
"WIP on add-pkg-commands-test: 55c6af2 increase parallel build",
},
{
Index: 1,
Name: "WIP on master: bb86a3f update github template",
1,
"WIP on master: bb86a3f update github template",
"WIP on master: bb86a3f update github template",
},
}
@@ -301,9 +312,9 @@ func TestGitCommandGetStashEntries(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
gitCmd.OSCommand.command = s.command
s.test(gitCmd.GetStashEntries(""))
s.test(gitCmd.GetStashEntries())
})
}
}
@@ -313,7 +324,7 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func([]*models.File)
test func([]*File)
}
scenarios := []scenario{
@@ -322,7 +333,7 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
func(cmd string, args ...string) *exec.Cmd {
return exec.Command("echo")
},
func(files []*models.File) {
func(files []*File) {
assert.Len(t, files, 0)
},
},
@@ -334,10 +345,10 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
"MM file1.txt\nA file3.txt\nAM file2.txt\n?? file4.txt\nUU file5.txt",
)
},
func(files []*models.File) {
func(files []*File) {
assert.Len(t, files, 5)
expected := []*models.File{
expected := []*File{
{
Name: "file1.txt",
HasStagedChanges: true,
@@ -408,9 +419,9 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
gitCmd.OSCommand.command = s.command
s.test(gitCmd.GetStatusFiles(GetStatusFileOptions{}))
s.test(gitCmd.GetStatusFiles())
})
}
}
@@ -418,7 +429,7 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
// TestGitCommandStashDo is a function.
func TestGitCommandStashDo(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"stash", "drop", "stash@{1}"}, args)
@@ -431,7 +442,7 @@ func TestGitCommandStashDo(t *testing.T) {
// TestGitCommandStashSave is a function.
func TestGitCommandStashSave(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"stash", "save", "A stash message"}, args)
@@ -444,7 +455,7 @@ func TestGitCommandStashSave(t *testing.T) {
// TestGitCommandCommitAmend is a function.
func TestGitCommandCommitAmend(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"commit", "--amend", "--allow-empty"}, args)
@@ -459,22 +470,22 @@ func TestGitCommandCommitAmend(t *testing.T) {
func TestGitCommandMergeStatusFiles(t *testing.T) {
type scenario struct {
testName string
oldFiles []*models.File
newFiles []*models.File
test func([]*models.File)
oldFiles []*File
newFiles []*File
test func([]*File)
}
scenarios := []scenario{
{
"Old file and new file are the same",
[]*models.File{},
[]*models.File{
[]*File{},
[]*File{
{
Name: "new_file.txt",
},
},
func(files []*models.File) {
expected := []*models.File{
func(files []*File) {
expected := []*File{
{
Name: "new_file.txt",
},
@@ -486,7 +497,7 @@ func TestGitCommandMergeStatusFiles(t *testing.T) {
},
{
"Several files to merge, with some identical",
[]*models.File{
[]*File{
{
Name: "new_file1.txt",
},
@@ -497,7 +508,7 @@ func TestGitCommandMergeStatusFiles(t *testing.T) {
Name: "new_file3.txt",
},
},
[]*models.File{
[]*File{
{
Name: "new_file4.txt",
},
@@ -508,8 +519,8 @@ func TestGitCommandMergeStatusFiles(t *testing.T) {
Name: "new_file1.txt",
},
},
func(files []*models.File) {
expected := []*models.File{
func(files []*File) {
expected := []*File{
{
Name: "new_file1.txt",
},
@@ -531,7 +542,7 @@ func TestGitCommandMergeStatusFiles(t *testing.T) {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
s.test(gitCmd.MergeStatusFiles(s.oldFiles, s.newFiles, nil))
s.test(gitCmd.MergeStatusFiles(s.oldFiles, s.newFiles))
})
}
}
@@ -588,7 +599,7 @@ func TestGitCommandGetCommitDifferences(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
gitCmd.OSCommand.command = s.command
s.test(gitCmd.GetCommitDifferences("HEAD", "@{u}"))
})
}
@@ -597,7 +608,7 @@ func TestGitCommandGetCommitDifferences(t *testing.T) {
// TestGitCommandRenameCommit is a function.
func TestGitCommandRenameCommit(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"commit", "--allow-empty", "--amend", "-m", "test"}, args)
@@ -610,27 +621,27 @@ func TestGitCommandRenameCommit(t *testing.T) {
// TestGitCommandResetToCommit is a function.
func TestGitCommandResetToCommit(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"reset", "--hard", "78976bc"}, args)
return exec.Command("echo")
}
assert.NoError(t, gitCmd.ResetToCommit("78976bc", "hard", oscommands.RunCommandOptions{}))
assert.NoError(t, gitCmd.ResetToCommit("78976bc", "hard"))
}
// TestGitCommandNewBranch is a function.
func TestGitCommandNewBranch(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"checkout", "-b", "test", "master"}, args)
assert.EqualValues(t, []string{"checkout", "-b", "test"}, args)
return exec.Command("echo")
}
assert.NoError(t, gitCmd.NewBranch("test", "master"))
assert.NoError(t, gitCmd.NewBranch("test"))
}
// TestGitCommandDeleteBranch is a function.
@@ -677,7 +688,7 @@ func TestGitCommandDeleteBranch(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
gitCmd.OSCommand.command = s.command
s.test(gitCmd.DeleteBranch(s.branch, s.force))
})
}
@@ -686,14 +697,14 @@ func TestGitCommandDeleteBranch(t *testing.T) {
// TestGitCommandMerge is a function.
func TestGitCommandMerge(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"merge", "--no-edit", "test"}, args)
return exec.Command("echo")
}
assert.NoError(t, gitCmd.Merge("test", MergeOpts{}))
assert.NoError(t, gitCmd.Merge("test"))
}
// TestGitCommandUsingGpg is a function.
@@ -805,7 +816,7 @@ func TestGitCommandCommit(t *testing.T) {
"Commit using gpg",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "bash", cmd)
assert.EqualValues(t, []string{"-c", "git commit -m \"test\""}, args)
assert.EqualValues(t, []string{"-c", `git commit -m 'test'`}, args)
return exec.Command("echo")
},
@@ -875,7 +886,7 @@ func TestGitCommandCommit(t *testing.T) {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.getGlobalGitConfig = s.getGlobalGitConfig
gitCmd.OSCommand.Command = s.command
gitCmd.OSCommand.command = s.command
s.test(gitCmd.Commit("test", s.flags))
})
}
@@ -945,7 +956,7 @@ func TestGitCommandAmendHead(t *testing.T) {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.getGlobalGitConfig = s.getGlobalGitConfig
gitCmd.OSCommand.Command = s.command
gitCmd.OSCommand.command = s.command
s.test(gitCmd.AmendHead())
})
}
@@ -1004,7 +1015,7 @@ func TestGitCommandPush(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
gitCmd.OSCommand.command = s.command
err := gitCmd.Push("test", s.forcePush, "", "", func(passOrUname string) string {
return "\n"
})
@@ -1013,18 +1024,11 @@ func TestGitCommandPush(t *testing.T) {
}
}
// TestGitCommandCatFile tests emitting a file using commands, where commands vary by OS.
// TestGitCommandCatFile is a function.
func TestGitCommandCatFile(t *testing.T) {
var osCmd string
switch os := runtime.GOOS; os {
case "windows":
osCmd = "type"
default:
osCmd = "cat"
}
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, osCmd, cmd)
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "cat", cmd)
assert.EqualValues(t, []string{"test.txt"}, args)
return exec.Command("echo", "-n", "test")
@@ -1038,7 +1042,7 @@ func TestGitCommandCatFile(t *testing.T) {
// TestGitCommandStageFile is a function.
func TestGitCommandStageFile(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"add", "test.txt"}, args)
@@ -1062,7 +1066,7 @@ func TestGitCommandUnstageFile(t *testing.T) {
"Remove an untracked file from staging",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"rm", "--cached", "--force", "test.txt"}, args)
assert.EqualValues(t, []string{"rm", "--cached", "test.txt"}, args)
return exec.Command("echo")
},
@@ -1089,19 +1093,88 @@ func TestGitCommandUnstageFile(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
gitCmd.OSCommand.command = s.command
s.test(gitCmd.UnStageFile("test.txt", s.tracked))
})
}
}
// 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 {
testName string
command func() (func(string, ...string) *exec.Cmd, *[][]string)
test func(*[][]string, error)
file *models.File
file *File
removeFile func(string) error
}
@@ -1123,7 +1196,7 @@ func TestGitCommandDiscardAllFileChanges(t *testing.T) {
{"reset", "--", "test"},
})
},
&models.File{
&File{
Name: "test",
HasStagedChanges: true,
},
@@ -1146,7 +1219,7 @@ func TestGitCommandDiscardAllFileChanges(t *testing.T) {
assert.EqualError(t, err, "an error occurred when removing file")
assert.Len(t, *cmdsCalled, 0)
},
&models.File{
&File{
Name: "test",
Tracked: false,
},
@@ -1171,7 +1244,7 @@ func TestGitCommandDiscardAllFileChanges(t *testing.T) {
{"checkout", "--", "test"},
})
},
&models.File{
&File{
Name: "test",
Tracked: true,
HasStagedChanges: false,
@@ -1197,7 +1270,7 @@ func TestGitCommandDiscardAllFileChanges(t *testing.T) {
{"checkout", "--", "test"},
})
},
&models.File{
&File{
Name: "test",
Tracked: true,
HasStagedChanges: false,
@@ -1224,7 +1297,7 @@ func TestGitCommandDiscardAllFileChanges(t *testing.T) {
{"checkout", "--", "test"},
})
},
&models.File{
&File{
Name: "test",
Tracked: true,
HasStagedChanges: true,
@@ -1251,7 +1324,7 @@ func TestGitCommandDiscardAllFileChanges(t *testing.T) {
{"checkout", "--", "test"},
})
},
&models.File{
&File{
Name: "test",
Tracked: true,
HasMergeConflicts: true,
@@ -1277,7 +1350,7 @@ func TestGitCommandDiscardAllFileChanges(t *testing.T) {
{"reset", "--", "test"},
})
},
&models.File{
&File{
Name: "test",
Tracked: false,
HasStagedChanges: true,
@@ -1301,7 +1374,7 @@ func TestGitCommandDiscardAllFileChanges(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, *cmdsCalled, 0)
},
&models.File{
&File{
Name: "test",
Tracked: false,
HasStagedChanges: false,
@@ -1317,7 +1390,7 @@ func TestGitCommandDiscardAllFileChanges(t *testing.T) {
t.Run(s.testName, func(t *testing.T) {
var cmdsCalled *[][]string
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command, cmdsCalled = s.command()
gitCmd.OSCommand.command, cmdsCalled = s.command()
gitCmd.removeFile = s.removeFile
s.test(cmdsCalled, gitCmd.DiscardAllFileChanges(s.file))
})
@@ -1365,8 +1438,8 @@ func TestGitCommandCheckout(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
s.test(gitCmd.Checkout("test", CheckoutOptions{Force: s.force}))
gitCmd.OSCommand.command = s.command
s.test(gitCmd.Checkout("test", s.force))
})
}
}
@@ -1374,11 +1447,13 @@ func TestGitCommandCheckout(t *testing.T) {
// TestGitCommandGetBranchGraph is a function.
func TestGitCommandGetBranchGraph(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
gitCmd.OSCommand.command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"log", "--graph", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium", "test", "--"}, args)
assert.EqualValues(t, []string{"log", "--graph", "--color", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium", "test"}, args)
return exec.Command("echo")
}
_, err := gitCmd.GetBranchGraph("test")
assert.NoError(t, err)
}
@@ -1388,7 +1463,7 @@ func TestGitCommandDiff(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
file *models.File
file *File
plain bool
cached bool
}
@@ -1398,11 +1473,11 @@ func TestGitCommandDiff(t *testing.T) {
"Default case",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=always", "--", "test.txt"}, args)
assert.EqualValues(t, []string{"diff", "--stat", "-p", "--color", "--", "test.txt"}, args)
return exec.Command("echo")
},
&models.File{
&File{
Name: "test.txt",
HasStagedChanges: false,
Tracked: true,
@@ -1414,11 +1489,11 @@ func TestGitCommandDiff(t *testing.T) {
"cached",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=always", "--cached", "--", "test.txt"}, args)
assert.EqualValues(t, []string{"diff", "--stat", "-p", "--color", "--cached", "--", "test.txt"}, args)
return exec.Command("echo")
},
&models.File{
&File{
Name: "test.txt",
HasStagedChanges: false,
Tracked: true,
@@ -1430,11 +1505,11 @@ func TestGitCommandDiff(t *testing.T) {
"plain",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=never", "--", "test.txt"}, args)
assert.EqualValues(t, []string{"diff", "--stat", "-p", "--", "test.txt"}, args)
return exec.Command("echo")
},
&models.File{
&File{
Name: "test.txt",
HasStagedChanges: false,
Tracked: true,
@@ -1446,11 +1521,11 @@ func TestGitCommandDiff(t *testing.T) {
"File not tracked and file has no staged changes",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=always", "--no-index", "/dev/null", "test.txt"}, args)
assert.EqualValues(t, []string{"diff", "--stat", "-p", "--color", "--no-index", "/dev/null", "test.txt"}, args)
return exec.Command("echo")
},
&models.File{
&File{
Name: "test.txt",
HasStagedChanges: false,
Tracked: false,
@@ -1463,8 +1538,8 @@ func TestGitCommandDiff(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
gitCmd.WorktreeFileDiff(s.file, s.plain, s.cached)
gitCmd.OSCommand.command = s.command
gitCmd.Diff(s.file, s.plain, s.cached)
})
}
}
@@ -1474,7 +1549,7 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(string, string, error)
test func(string, error)
}
scenarios := []scenario{
@@ -1484,10 +1559,9 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
assert.Equal(t, "git", cmd)
return exec.Command("echo", "master")
},
func(name string, displayname string, err error) {
func(output string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "master", name)
assert.EqualValues(t, "master", displayname)
assert.EqualValues(t, "master", output)
},
},
{
@@ -1506,32 +1580,9 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
return nil
},
func(name string, displayname string, err error) {
func(output string, err error) {
assert.NoError(t, err)
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)
assert.EqualValues(t, "master", output)
},
},
{
@@ -1540,10 +1591,9 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
assert.Equal(t, "git", cmd)
return exec.Command("test")
},
func(name string, displayname string, err error) {
func(output string, err error) {
assert.Error(t, err)
assert.EqualValues(t, "", name)
assert.EqualValues(t, "", displayname)
assert.EqualValues(t, "", output)
},
},
}
@@ -1551,7 +1601,7 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
gitCmd.OSCommand.command = s.command
s.test(gitCmd.CurrentBranchName())
})
}
@@ -1608,7 +1658,7 @@ func TestGitCommandApplyPatch(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
gitCmd.OSCommand.command = s.command
s.test(gitCmd.ApplyPatch("test", "cached"))
})
}
@@ -1629,7 +1679,7 @@ func TestGitCommandRebaseBranch(t *testing.T) {
"master",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: "git rebase --interactive --autostash --keep-empty master",
Expect: "git rebase --interactive --autostash --keep-empty --rebase-merges master",
Replace: "echo",
},
}),
@@ -1642,7 +1692,7 @@ func TestGitCommandRebaseBranch(t *testing.T) {
"master",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: "git rebase --interactive --autostash --keep-empty master",
Expect: "git rebase --interactive --autostash --keep-empty --rebase-merges master",
Replace: "test",
},
}),
@@ -1656,7 +1706,7 @@ func TestGitCommandRebaseBranch(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd.OSCommand.Command = s.command
gitCmd.OSCommand.command = s.command
s.test(gitCmd.RebaseBranch(s.arg))
})
}
@@ -1707,7 +1757,7 @@ func TestGitCommandCheckoutFile(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd.OSCommand.Command = s.command
gitCmd.OSCommand.command = s.command
s.test(gitCmd.CheckoutFile(s.commitSha, s.fileName))
})
}
@@ -1718,7 +1768,7 @@ func TestGitCommandDiscardOldFileChanges(t *testing.T) {
type scenario struct {
testName string
getLocalGitConfig func(string) (string, error)
commits []*models.Commit
commits []*Commit
commitIndex int
fileName string
command func(string, ...string) *exec.Cmd
@@ -1731,7 +1781,7 @@ func TestGitCommandDiscardOldFileChanges(t *testing.T) {
func(string) (string, error) {
return "", nil
},
[]*models.Commit{},
[]*Commit{},
0,
"test999.txt",
nil,
@@ -1744,7 +1794,7 @@ func TestGitCommandDiscardOldFileChanges(t *testing.T) {
func(string) (string, error) {
return "true", nil
},
[]*models.Commit{{Name: "commit", Sha: "123456"}},
[]*Commit{{Name: "commit", Sha: "123456"}},
0,
"test999.txt",
nil,
@@ -1757,7 +1807,7 @@ func TestGitCommandDiscardOldFileChanges(t *testing.T) {
func(string) (string, error) {
return "", nil
},
[]*models.Commit{
[]*Commit{
{Name: "commit", Sha: "123456"},
{Name: "commit2", Sha: "abcdef"},
},
@@ -1765,7 +1815,7 @@ func TestGitCommandDiscardOldFileChanges(t *testing.T) {
"test999.txt",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: "git rebase --interactive --autostash --keep-empty abcdef",
Expect: "git rebase --interactive --autostash --keep-empty --rebase-merges abcdef",
Replace: "echo",
},
{
@@ -1797,18 +1847,95 @@ func TestGitCommandDiscardOldFileChanges(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd.OSCommand.Command = s.command
gitCmd.OSCommand.command = s.command
gitCmd.getLocalGitConfig = s.getLocalGitConfig
s.test(gitCmd.DiscardOldFileChanges(s.commits, s.commitIndex, s.fileName))
})
}
}
// TestGitCommandShowCommitFile is a function.
func TestGitCommandShowCommitFile(t *testing.T) {
type scenario struct {
testName string
commitSha string
fileName string
command func(string, ...string) *exec.Cmd
test func(string, error)
}
scenarios := []scenario{
{
"valid case",
"123456",
"hello.txt",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: "git show --no-renames 123456 -- hello.txt",
Replace: "echo -n hello",
},
}),
func(str string, err error) {
assert.NoError(t, err)
assert.Equal(t, "hello", str)
},
},
}
gitCmd := NewDummyGitCommand()
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd.OSCommand.command = s.command
s.test(gitCmd.ShowCommitFile(s.commitSha, s.fileName, true))
})
}
}
// TestGitCommandGetCommitFiles is a function.
func TestGitCommandGetCommitFiles(t *testing.T) {
type scenario struct {
testName string
commitSha string
command func(string, ...string) *exec.Cmd
test func([]*CommitFile, error)
}
scenarios := []scenario{
{
"valid case",
"123456",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: "git show --pretty= --name-only --no-renames 123456",
Replace: "echo 'hello\nworld'",
},
}),
func(commitFiles []*CommitFile, err error) {
assert.NoError(t, err)
assert.Equal(t, []*CommitFile{
{Sha: "123456", Name: "hello", DisplayString: "hello"},
{Sha: "123456", Name: "world", DisplayString: "world"},
}, commitFiles)
},
},
}
gitCmd := NewDummyGitCommand()
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd.OSCommand.command = s.command
s.test(gitCmd.GetCommitFiles(s.commitSha, nil))
})
}
}
// TestGitCommandDiscardUnstagedFileChanges is a function.
func TestGitCommandDiscardUnstagedFileChanges(t *testing.T) {
type scenario struct {
testName string
file *models.File
file *File
command func(string, ...string) *exec.Cmd
test func(error)
}
@@ -1816,7 +1943,7 @@ func TestGitCommandDiscardUnstagedFileChanges(t *testing.T) {
scenarios := []scenario{
{
"valid case",
&models.File{Name: "test.txt"},
&File{Name: "test.txt"},
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: `git checkout -- "test.txt"`,
@@ -1833,7 +1960,7 @@ func TestGitCommandDiscardUnstagedFileChanges(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd.OSCommand.Command = s.command
gitCmd.OSCommand.command = s.command
s.test(gitCmd.DiscardUnstagedFileChanges(s.file))
})
}
@@ -1866,7 +1993,7 @@ func TestGitCommandDiscardAnyUnstagedFileChanges(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd.OSCommand.Command = s.command
gitCmd.OSCommand.command = s.command
s.test(gitCmd.DiscardAnyUnstagedFileChanges())
})
}
@@ -1899,7 +2026,7 @@ func TestGitCommandRemoveUntrackedFiles(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd.OSCommand.Command = s.command
gitCmd.OSCommand.command = s.command
s.test(gitCmd.RemoveUntrackedFiles())
})
}
@@ -1934,7 +2061,7 @@ func TestGitCommandResetHard(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd.OSCommand.Command = s.command
gitCmd.OSCommand.command = s.command
s.test(gitCmd.ResetHard(s.ref))
})
}
@@ -1969,7 +2096,7 @@ func TestGitCommandCreateFixupCommit(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd.OSCommand.Command = s.command
gitCmd.OSCommand.command = s.command
s.test(gitCmd.CreateFixupCommit(s.sha))
})
}
@@ -1995,13 +2122,6 @@ 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,
@@ -2010,7 +2130,7 @@ func TestGitCommandSkipEditorCommand(t *testing.T) {
)
})
_ = cmd.runSkipEditorCommand("true")
cmd.RunSkipEditorCommand("true")
}
func TestFindDotGitDir(t *testing.T) {

View File

@@ -1,161 +0,0 @@
package commands
import (
"regexp"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
// context:
// we want to only show 'safe' branches (ones that haven't e.g. been deleted)
// which `git branch -a` gives us, but we also want the recency data that
// git reflog gives us.
// So we get the HEAD, then append get the reflog branches that intersect with
// our safe branches, then add the remaining safe branches, ensuring uniqueness
// along the way
// if we find out we need to use one of these functions in the git.go file, we
// can just pull them out of here and put them there and then call them from in here
// BranchListBuilder returns a list of Branch objects for the current repo
type BranchListBuilder struct {
Log *logrus.Entry
GitCommand *GitCommand
ReflogCommits []*models.Commit
}
// NewBranchListBuilder builds a new branch list builder
func NewBranchListBuilder(log *logrus.Entry, gitCommand *GitCommand, reflogCommits []*models.Commit) (*BranchListBuilder, error) {
return &BranchListBuilder{
Log: log,
GitCommand: gitCommand,
ReflogCommits: reflogCommits,
}, nil
}
func (b *BranchListBuilder) obtainBranches() []*models.Branch {
cmdStr := `git for-each-ref --sort=-committerdate --format="%(HEAD)|%(refname:short)|%(upstream:short)|%(upstream:track)" refs/heads`
output, err := b.GitCommand.OSCommand.RunCommandWithOutput(cmdStr)
if err != nil {
panic(err)
}
trimmedOutput := strings.TrimSpace(output)
outputLines := strings.Split(trimmedOutput, "\n")
branches := make([]*models.Branch, 0, len(outputLines))
for _, line := range outputLines {
if line == "" {
continue
}
split := strings.Split(line, SEPARATION_CHAR)
name := strings.TrimPrefix(split[1], "heads/")
branch := &models.Branch{
Name: name,
Pullables: "?",
Pushables: "?",
Head: split[0] == "*",
}
upstreamName := split[2]
if upstreamName == "" {
branches = append(branches, branch)
continue
}
branch.UpstreamName = upstreamName
track := split[3]
re := regexp.MustCompile(`ahead (\d+)`)
match := re.FindStringSubmatch(track)
if len(match) > 1 {
branch.Pushables = match[1]
} else {
branch.Pushables = "0"
}
re = regexp.MustCompile(`behind (\d+)`)
match = re.FindStringSubmatch(track)
if len(match) > 1 {
branch.Pullables = match[1]
} else {
branch.Pullables = "0"
}
branches = append(branches, branch)
}
return branches
}
// Build the list of branches for the current repo
func (b *BranchListBuilder) Build() []*models.Branch {
branches := b.obtainBranches()
reflogBranches := b.obtainReflogBranches()
// loop through reflog branches. If there is a match, merge them, then remove it from the branches and keep it in the reflog branches
branchesWithRecency := make([]*models.Branch, 0)
outer:
for _, reflogBranch := range reflogBranches {
for j, branch := range branches {
if branch.Head {
continue
}
if strings.EqualFold(reflogBranch.Name, branch.Name) {
branch.Recency = reflogBranch.Recency
branchesWithRecency = append(branchesWithRecency, branch)
branches = append(branches[0:j], branches[j+1:]...)
continue outer
}
}
}
branches = append(branchesWithRecency, branches...)
foundHead := false
for i, branch := range branches {
if branch.Head {
foundHead = true
branch.Recency = " *"
branches = append(branches[0:i], branches[i+1:]...)
branches = append([]*models.Branch{branch}, branches...)
break
}
}
if !foundHead {
currentBranchName, currentBranchDisplayName, err := b.GitCommand.CurrentBranchName()
if err != nil {
panic(err)
}
branches = append([]*models.Branch{{Name: currentBranchName, DisplayName: currentBranchDisplayName, Head: true, Recency: " *"}}, branches...)
}
return branches
}
// TODO: only look at the new reflog commits, and otherwise store the recencies in
// int form against the branch to recalculate the time ago
func (b *BranchListBuilder) obtainReflogBranches() []*models.Branch {
foundBranchesMap := map[string]bool{}
re := regexp.MustCompile(`checkout: moving from ([\S]+) to ([\S]+)`)
reflogBranches := make([]*models.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, &models.Branch{
Recency: recency,
Name: branchName,
})
}
}
}
}
return reflogBranches
}

View File

@@ -1,50 +0,0 @@
package commands
import (
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
)
// GetFilesInDiff get the specified commit files
func (c *GitCommand) GetFilesInDiff(from string, to string, reverse bool, patchManager *patch.PatchManager) ([]*models.CommitFile, error) {
reverseFlag := ""
if reverse {
reverseFlag = " -R "
}
filenames, err := c.OSCommand.RunCommandWithOutput("git diff --submodule --no-ext-diff --name-status %s %s %s", reverseFlag, from, to)
if err != nil {
return nil, err
}
return c.getCommitFilesFromFilenames(filenames, to, patchManager), nil
}
// filenames string is something like "file1\nfile2\nfile3"
func (c *GitCommand) getCommitFilesFromFilenames(filenames string, parent string, patchManager *patch.PatchManager) []*models.CommitFile {
commitFiles := make([]*models.CommitFile, 0)
for _, line := range strings.Split(strings.TrimRight(filenames, "\n"), "\n") {
// typical result looks like 'A my_file' meaning my_file was added
if line == "" {
continue
}
changeStatus := line[0:1]
name := line[2:]
status := patch.UNSELECTED
if patchManager != nil && patchManager.To == parent {
status = patchManager.GetFileStatus(name)
}
commitFiles = append(commitFiles, &models.CommitFile{
Parent: parent,
Name: name,
ChangeStatus: changeStatus,
PatchStatus: status,
})
}
return commitFiles
}

View File

@@ -1,381 +0,0 @@
package commands
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/sirupsen/logrus"
)
// context:
// here we get the commits from git log but format them to show whether they're
// unpushed/pushed/merged into the base branch or not, or if they're yet to
// be processed as part of a rebase (these won't appear in git log but we
// grab them from the rebase-related files in the .git directory to show them
// if we find out we need to use one of these functions in the git.go file, we
// can just pull them out of here and put them there and then call them from in here
const SEPARATION_CHAR = "|"
// CommitListBuilder returns a list of Branch objects for the current repo
type CommitListBuilder struct {
Log *logrus.Entry
GitCommand *GitCommand
OSCommand *oscommands.OSCommand
Tr *i18n.TranslationSet
}
// NewCommitListBuilder builds a new commit list builder
func NewCommitListBuilder(log *logrus.Entry, gitCommand *GitCommand, osCommand *oscommands.OSCommand, tr *i18n.TranslationSet) *CommitListBuilder {
return &CommitListBuilder{
Log: log,
GitCommand: gitCommand,
OSCommand: osCommand,
Tr: tr,
}
}
// extractCommitFromLine takes a line from a git log and extracts the sha, message, date, and tag if present
// then puts them into a commit object
// example input:
// 8ad01fe32fcc20f07bc6693f87aa4977c327f1e1|10 hours ago|Jesse Duffield| (HEAD -> master, tag: v0.15.2)|refresh commits when adding a tag
func (c *CommitListBuilder) extractCommitFromLine(line string) *models.Commit {
split := strings.Split(line, SEPARATION_CHAR)
sha := split[0]
unixTimestamp := split[1]
author := split[2]
extraInfo := strings.TrimSpace(split[3])
parentHashes := split[4]
message := strings.Join(split[5:], SEPARATION_CHAR)
tags := []string{}
if extraInfo != "" {
re := regexp.MustCompile(`tag: ([^,\)]+)`)
tagMatch := re.FindStringSubmatch(extraInfo)
if len(tagMatch) > 1 {
tags = append(tags, tagMatch[1])
}
}
unitTimestampInt, _ := strconv.Atoi(unixTimestamp)
// Any commit with multiple parents is a merge commit.
// If there's a space then it means there must be more than one parent hash
isMerge := strings.Contains(parentHashes, " ")
return &models.Commit{
Sha: sha,
Name: message,
Tags: tags,
ExtraInfo: extraInfo,
UnixTimestamp: int64(unitTimestampInt),
Author: author,
IsMerge: isMerge,
}
}
type GetCommitsOptions struct {
Limit bool
FilterPath string
IncludeRebaseCommits bool
RefName string // e.g. "HEAD" or "my_branch"
}
func (c *CommitListBuilder) MergeRebasingCommits(commits []*models.Commit) ([]*models.Commit, error) {
// chances are we have as many commits as last time so we'll set the capacity to be the old length
result := make([]*models.Commit, 0, len(commits))
for i, commit := range commits {
if commit.Status != "rebasing" { // removing the existing rebase commits so we can add the refreshed ones
result = append(result, commits[i:]...)
break
}
}
rebaseMode, err := c.GitCommand.RebaseMode()
if err != nil {
return nil, err
}
if rebaseMode == "" {
// not in rebase mode so return original commits
return result, nil
}
rebasingCommits, err := c.getRebasingCommits(rebaseMode)
if err != nil {
return nil, err
}
if len(rebasingCommits) > 0 {
result = append(rebasingCommits, result...)
}
return result, nil
}
// GetCommits obtains the commits of the current branch
func (c *CommitListBuilder) GetCommits(opts GetCommitsOptions) ([]*models.Commit, error) {
commits := []*models.Commit{}
var rebasingCommits []*models.Commit
rebaseMode, err := c.GitCommand.RebaseMode()
if err != nil {
return nil, err
}
if opts.IncludeRebaseCommits && opts.FilterPath == "" {
var err error
rebasingCommits, err = c.MergeRebasingCommits(commits)
if err != nil {
return nil, err
}
commits = append(commits, rebasingCommits...)
}
passedFirstPushedCommit := false
firstPushedCommit, err := c.getFirstPushedCommit(opts.RefName)
if err != nil {
// must have no upstream branch so we'll consider everything as pushed
passedFirstPushedCommit = true
}
cmd := c.getLogCmd(opts)
err = oscommands.RunLineOutputCmd(cmd, func(line string) (bool, error) {
if strings.Split(line, " ")[0] != "gpg:" {
commit := c.extractCommitFromLine(line)
if commit.Sha == firstPushedCommit {
passedFirstPushedCommit = true
}
commit.Status = map[bool]string{true: "unpushed", false: "pushed"}[!passedFirstPushedCommit]
commits = append(commits, commit)
}
return false, nil
})
if err != nil {
return nil, err
}
if rebaseMode != "" {
currentCommit := commits[len(rebasingCommits)]
blue := color.New(color.FgYellow)
youAreHere := blue.Sprintf("<-- %s ---", c.Tr.YouAreHere)
currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name)
}
commits, err = c.setCommitMergedStatuses(opts.RefName, commits)
if err != nil {
return nil, err
}
return commits, nil
}
// getRebasingCommits obtains the commits that we're in the process of rebasing
func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*models.Commit, error) {
switch rebaseMode {
case "normal":
return c.getNormalRebasingCommits()
case "interactive":
return c.getInteractiveRebasingCommits()
default:
return nil, nil
}
}
func (c *CommitListBuilder) getNormalRebasingCommits() ([]*models.Commit, error) {
rewrittenCount := 0
bytesContent, err := ioutil.ReadFile(filepath.Join(c.GitCommand.DotGitDir, "rebase-apply/rewritten"))
if err == nil {
content := string(bytesContent)
rewrittenCount = len(strings.Split(content, "\n"))
}
// we know we're rebasing, so lets get all the files whose names have numbers
commits := []*models.Commit{}
err = filepath.Walk(filepath.Join(c.GitCommand.DotGitDir, "rebase-apply"), func(path string, f os.FileInfo, err error) error {
if rewrittenCount > 0 {
rewrittenCount--
return nil
}
if err != nil {
return err
}
re := regexp.MustCompile(`^\d+$`)
if !re.MatchString(f.Name()) {
return nil
}
bytesContent, err := ioutil.ReadFile(path)
if err != nil {
return err
}
content := string(bytesContent)
commit, err := c.commitFromPatch(content)
if err != nil {
return err
}
commits = append([]*models.Commit{commit}, commits...)
return nil
})
if err != nil {
return nil, err
}
return commits, nil
}
// git-rebase-todo example:
// pick ac446ae94ee560bdb8d1d057278657b251aaef17 ac446ae
// pick afb893148791a2fbd8091aeb81deba4930c73031 afb8931
// git-rebase-todo.backup example:
// pick 49cbba374296938ea86bbd4bf4fee2f6ba5cccf6 third commit on master
// pick ac446ae94ee560bdb8d1d057278657b251aaef17 blah commit on master
// pick afb893148791a2fbd8091aeb81deba4930c73031 fourth commit on master
// getInteractiveRebasingCommits takes our git-rebase-todo and our git-rebase-todo.backup files
// and extracts out the sha and names of commits that we still have to go
// in the rebase:
func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*models.Commit, error) {
bytesContent, err := ioutil.ReadFile(filepath.Join(c.GitCommand.DotGitDir, "rebase-merge/git-rebase-todo"))
if err != nil {
c.Log.Error(fmt.Sprintf("error occurred reading git-rebase-todo: %s", err.Error()))
// we assume an error means the file doesn't exist so we just return
return nil, nil
}
commits := []*models.Commit{}
lines := strings.Split(string(bytesContent), "\n")
for _, line := range lines {
if line == "" || line == "noop" {
return commits, nil
}
if strings.HasPrefix(line, "#") {
continue
}
splitLine := strings.Split(line, " ")
commits = append([]*models.Commit{{
Sha: splitLine[1],
Name: strings.Join(splitLine[2:], " "),
Status: "rebasing",
Action: splitLine[0],
}}, commits...)
}
return commits, nil
}
// assuming the file starts like this:
// From e93d4193e6dd45ca9cf3a5a273d7ba6cd8b8fb20 Mon Sep 17 00:00:00 2001
// From: Lazygit Tester <test@example.com>
// Date: Wed, 5 Dec 2018 21:03:23 +1100
// Subject: second commit on master
func (c *CommitListBuilder) commitFromPatch(content string) (*models.Commit, error) {
lines := strings.Split(content, "\n")
sha := strings.Split(lines[0], " ")[1]
name := strings.TrimPrefix(lines[3], "Subject: ")
return &models.Commit{
Sha: sha,
Name: name,
Status: "rebasing",
}, nil
}
func (c *CommitListBuilder) setCommitMergedStatuses(refName string, commits []*models.Commit) ([]*models.Commit, error) {
ancestor, err := c.getMergeBase(refName)
if err != nil {
return nil, err
}
if ancestor == "" {
return commits, nil
}
passedAncestor := false
for i, commit := range commits {
if strings.HasPrefix(ancestor, commit.Sha) {
passedAncestor = true
}
if commit.Status != "pushed" {
continue
}
if passedAncestor {
commits[i].Status = "merged"
}
}
return commits, nil
}
func (c *CommitListBuilder) getMergeBase(refName string) (string, error) {
currentBranch, _, err := c.GitCommand.CurrentBranchName()
if err != nil {
return "", err
}
baseBranch := "master"
if strings.HasPrefix(currentBranch, "feature/") {
baseBranch = "develop"
}
// swallowing error because it's not a big deal; probably because there are no commits yet
output, _ := c.OSCommand.RunCommandWithOutput("git merge-base %s %s", refName, baseBranch)
return ignoringWarnings(output), nil
}
func ignoringWarnings(commandOutput string) string {
trimmedOutput := strings.TrimSpace(commandOutput)
split := strings.Split(trimmedOutput, "\n")
// need to get last line in case the first line is a warning about how the error is ambiguous.
// At some point we should find a way to make it unambiguous
lastLine := split[len(split)-1]
return lastLine
}
// getFirstPushedCommit returns the first commit SHA which has been pushed to the ref's upstream.
// all commits above this are deemed unpushed and marked as such.
func (c *CommitListBuilder) getFirstPushedCommit(refName string) (string, error) {
output, err := c.OSCommand.RunCommandWithOutput("git merge-base %s %s@{u}", refName, refName)
if err != nil {
return "", err
}
return ignoringWarnings(output), nil
}
// getLog gets the git log.
func (c *CommitListBuilder) getLogCmd(opts GetCommitsOptions) *exec.Cmd {
limitFlag := ""
if opts.Limit {
limitFlag = "-300"
}
filterFlag := ""
if opts.FilterPath != "" {
filterFlag = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(opts.FilterPath))
}
return c.OSCommand.ExecutableFromString(
fmt.Sprintf(
"git log %s --oneline --pretty=format:\"%%H%s%%at%s%%aN%s%%d%s%%p%s%%s\" %s --abbrev=%d --date=unix %s",
opts.RefName,
SEPARATION_CHAR,
SEPARATION_CHAR,
SEPARATION_CHAR,
SEPARATION_CHAR,
SEPARATION_CHAR,
limitFlag,
20,
filterFlag,
),
)
}

View File

@@ -1,113 +0,0 @@
package commands
import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
)
// NewDummyCommitListBuilder creates a new dummy CommitListBuilder for testing
func NewDummyCommitListBuilder() *CommitListBuilder {
osCommand := oscommands.NewDummyOSCommand()
return &CommitListBuilder{
Log: utils.NewDummyLog(),
GitCommand: NewDummyGitCommandWithOSCommand(osCommand),
OSCommand: osCommand,
Tr: i18n.NewTranslationSet(utils.NewDummyLog()),
}
}
// TestCommitListBuilderGetMergeBase is a function.
func TestCommitListBuilderGetMergeBase(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(string, error)
}
scenarios := []scenario{
{
"swallows an error if the call to merge-base returns an error",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("test")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "", output)
},
},
{
"returns the commit when master",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "master")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return exec.Command("echo", "blah")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.Equal(t, "blah", output)
},
},
{
"checks against develop when a feature branch",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return exec.Command("echo", "feature/test")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "develop"}, args)
return exec.Command("echo", "blah")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.Equal(t, "blah", output)
},
},
{
"bubbles up error if there is one",
func(cmd string, args ...string) *exec.Cmd {
return exec.Command("test")
},
func(output string, err error) {
assert.Error(t, err)
assert.Equal(t, "", output)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.getMergeBase("HEAD"))
})
}
}

View File

@@ -1,111 +0,0 @@
package commands
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// GetStatusFiles git status files
type GetStatusFileOptions struct {
NoRenames bool
}
func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
// check if config wants us ignoring untracked files
untrackedFilesSetting := c.GetConfigValue("status.showUntrackedFiles")
if untrackedFilesSetting == "" {
untrackedFilesSetting = "all"
}
untrackedFilesArg := fmt.Sprintf("--untracked-files=%s", untrackedFilesSetting)
statusOutput, err := c.GitStatus(GitStatusOptions{NoRenames: opts.NoRenames, UntrackedFilesArg: untrackedFilesArg})
if err != nil {
c.Log.Error(err)
}
statusStrings := utils.SplitLines(statusOutput)
files := []*models.File{}
for _, statusString := range statusStrings {
if strings.HasPrefix(statusString, "warning") {
c.Log.Warningf("warning when calling git status: %s", statusString)
continue
}
change := statusString[0:2]
stagedChange := change[0:1]
unstagedChange := statusString[1:2]
filename := c.OSCommand.Unquote(statusString[3:])
untracked := utils.IncludesString([]string{"??", "A ", "AM"}, change)
hasNoStagedChanges := utils.IncludesString([]string{" ", "U", "?"}, stagedChange)
hasMergeConflicts := utils.IncludesString([]string{"DD", "AA", "UU", "AU", "UA", "UD", "DU"}, change)
hasInlineMergeConflicts := utils.IncludesString([]string{"UU", "AA"}, change)
file := &models.File{
Name: filename,
DisplayString: statusString,
HasStagedChanges: !hasNoStagedChanges,
HasUnstagedChanges: unstagedChange != " ",
Tracked: !untracked,
Deleted: unstagedChange == "D" || stagedChange == "D",
HasMergeConflicts: hasMergeConflicts,
HasInlineMergeConflicts: hasInlineMergeConflicts,
Type: c.OSCommand.FileType(filename),
ShortStatus: change,
}
files = append(files, file)
}
return files
}
// GitStatus returns the plaintext short status of the repo
type GitStatusOptions struct {
NoRenames bool
UntrackedFilesArg string
}
func (c *GitCommand) GitStatus(opts GitStatusOptions) (string, error) {
noRenamesFlag := ""
if opts.NoRenames {
noRenamesFlag = "--no-renames"
}
return c.OSCommand.RunCommandWithOutput("git status %s --porcelain %s", opts.UntrackedFilesArg, noRenamesFlag)
}
// MergeStatusFiles merge status files
func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []*models.File, selectedFile *models.File) []*models.File {
if len(oldFiles) == 0 {
return newFiles
}
appendedIndexes := []int{}
// retain position of files we already could see
result := []*models.File{}
for _, oldFile := range oldFiles {
for newIndex, newFile := range newFiles {
if utils.IncludesInt(appendedIndexes, newIndex) {
continue
}
// if we just staged B and in doing so created 'A -> B' and we are currently have oldFile: A and newFile: 'A -> B', we want to wait until we come across B so the our cursor isn't jumping anywhere
waitForMatchingFile := selectedFile != nil && newFile.IsRename() && !selectedFile.IsRename() && newFile.Matches(selectedFile) && !oldFile.Matches(selectedFile)
if oldFile.Matches(newFile) && !waitForMatchingFile {
result = append(result, newFile)
appendedIndexes = append(appendedIndexes, newIndex)
}
}
}
// append any new files to the end
for index, newFile := range newFiles {
if !utils.IncludesInt(appendedIndexes, index) {
result = append(result, newFile)
}
}
return result
}

View File

@@ -1,54 +0,0 @@
package commands
import (
"fmt"
"regexp"
"strconv"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
)
// 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 *models.Commit, filterPath string) ([]*models.Commit, bool, error) {
commits := make([]*models.Commit, 0)
re := regexp.MustCompile(`(\w+).*HEAD@\{([^\}]+)\}: (.*)`)
filterPathArg := ""
if filterPath != "" {
filterPathArg = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(filterPath))
}
cmd := c.OSCommand.ExecutableFromString(fmt.Sprintf("git reflog --abbrev=20 --date=unix %s", filterPathArg))
onlyObtainedNewReflogCommits := false
err := oscommands.RunLineOutputCmd(cmd, func(line string) (bool, error) {
match := re.FindStringSubmatch(line)
if len(match) <= 1 {
return false, nil
}
unixTimestamp, _ := strconv.Atoi(match[2])
commit := &models.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, onlyObtainedNewReflogCommits, nil
}

View File

@@ -5,11 +5,9 @@ import (
"regexp"
"sort"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
)
func (c *GitCommand) GetRemotes() ([]*models.Remote, error) {
func (c *GitCommand) GetRemotes() ([]*Remote, error) {
// get remote branches
unescaped := "git branch -r"
remoteBranchesStr, err := c.OSCommand.RunCommandWithOutput(unescaped)
@@ -23,21 +21,21 @@ func (c *GitCommand) GetRemotes() ([]*models.Remote, error) {
}
// first step is to get our remotes from go-git
remotes := make([]*models.Remote, len(goGitRemotes))
remotes := make([]*Remote, len(goGitRemotes))
for i, goGitRemote := range goGitRemotes {
remoteName := goGitRemote.Config().Name
re := regexp.MustCompile(fmt.Sprintf(`%s\/([\S]+)`, remoteName))
matches := re.FindAllStringSubmatch(remoteBranchesStr, -1)
branches := make([]*models.RemoteBranch, len(matches))
branches := make([]*RemoteBranch, len(matches))
for j, match := range matches {
branches[j] = &models.RemoteBranch{
branches[j] = &RemoteBranch{
Name: match[1],
RemoteName: remoteName,
}
}
remotes[i] = &models.Remote{
remotes[i] = &Remote{
Name: goGitRemote.Config().Name,
Urls: goGitRemote.Config().URLs,
Branches: branches,

View File

@@ -1,67 +0,0 @@
package commands
import (
"fmt"
"regexp"
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (c *GitCommand) getUnfilteredStashEntries() []*models.StashEntry {
unescaped := "git stash list --pretty='%gs'"
rawString, _ := c.OSCommand.RunCommandWithOutput(unescaped)
stashEntries := []*models.StashEntry{}
for i, line := range utils.SplitLines(rawString) {
stashEntries = append(stashEntries, stashEntryFromLine(line, i))
}
return stashEntries
}
// GetStashEntries stash entries
func (c *GitCommand) GetStashEntries(filterPath string) []*models.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 := []*models.StashEntry{}
var currentStashEntry *models.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) *models.StashEntry {
return &models.StashEntry{
Name: line,
Index: index,
}
}

View File

@@ -6,7 +6,6 @@ import (
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -20,7 +19,7 @@ func convertToInt(s string) int {
return i
}
func (c *GitCommand) GetTags() ([]*models.Tag, error) {
func (c *GitCommand) GetTags() ([]*Tag, error) {
// get remote branches
remoteBranchesStr, err := c.OSCommand.RunCommandWithOutput(`git tag --list`)
if err != nil {
@@ -35,10 +34,10 @@ func (c *GitCommand) GetTags() ([]*models.Tag, error) {
split := strings.Split(content, "\n")
// first step is to get our remotes from go-git
tags := make([]*models.Tag, len(split))
tags := make([]*Tag, len(split))
for i, tagName := range split {
tags[i] = &models.Tag{
tags[i] = &Tag{
Name: tagName,
}
}

View File

@@ -1,26 +0,0 @@
package models
// Branch : A git branch
// duplicating this for now
type Branch struct {
Name string
// the displayname is something like '(HEAD detached at 123asdf)', whereas in that case the name would be '123asdf'
DisplayName string
Recency string
Pushables string
Pullables string
UpstreamName string
Head bool
}
func (b *Branch) RefName() string {
return b.Name
}
func (b *Branch) ID() string {
return b.RefName()
}
func (b *Branch) Description() string {
return b.RefName()
}

View File

@@ -1,37 +0,0 @@
package models
import "fmt"
// Commit : A git commit
type Commit struct {
Sha string
Name string
Status string // one of "unpushed", "pushed", "merged", "rebasing" or "selected"
Action string // one of "", "pick", "edit", "squash", "reword", "drop", "fixup"
Tags []string
ExtraInfo string // something like 'HEAD -> master, tag: v0.15.2'
Author string
UnixTimestamp int64
// IsMerge tells us whether we're dealing with a merge commit i.e. a commit with two parents
IsMerge bool
}
func (c *Commit) ShortSha() string {
if len(c.Sha) < 8 {
return c.Sha
}
return c.Sha[:8]
}
func (c *Commit) RefName() string {
return c.Sha
}
func (c *Commit) ID() string {
return c.RefName()
}
func (c *Commit) Description() string {
return fmt.Sprintf("%s %s", c.Sha[:7], c.Name)
}

View File

@@ -1,21 +0,0 @@
package models
// CommitFile : A git commit file
type CommitFile struct {
// Parent is the identifier of the parent object e.g. a commit SHA if this commit file is for a commit, or a stash entry ref like 'stash@{1}'
Parent string
Name string
// PatchStatus tells us whether the file has been wholly or partially added to a patch. We might want to pull this logic up into the gui package and make it a map like we do with cherry picked commits
PatchStatus int // one of 'WHOLE' 'PART' 'NONE'
ChangeStatus string // e.g. 'A' for added or 'M' for modified. This is based on the result from git diff --name-status
}
func (f *CommitFile) ID() string {
return f.Name
}
func (f *CommitFile) Description() string {
return f.Name
}

View File

@@ -1,60 +0,0 @@
package models
import (
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// File : A file from git status
// duplicating this for now
type File struct {
Name string
HasStagedChanges bool
HasUnstagedChanges bool
Tracked bool
Deleted bool
HasMergeConflicts bool
HasInlineMergeConflicts bool
DisplayString string
Type string // one of 'file', 'directory', and 'other'
ShortStatus string // e.g. 'AD', ' A', 'M ', '??'
}
const RENAME_SEPARATOR = " -> "
func (f *File) IsRename() bool {
return strings.Contains(f.Name, RENAME_SEPARATOR)
}
// Names returns an array containing just the filename, or in the case of a rename, the after filename and the before filename
func (f *File) Names() []string {
return strings.Split(f.Name, RENAME_SEPARATOR)
}
// returns true if the file names are the same or if a a file rename includes the filename of the other
func (f *File) Matches(f2 *File) bool {
return utils.StringArraysOverlap(f.Names(), f2.Names())
}
func (f *File) ID() string {
return f.Name
}
func (f *File) Description() string {
return f.Name
}
func (f *File) IsSubmodule(configs []*SubmoduleConfig) bool {
return f.SubmoduleConfig(configs) != nil
}
func (f *File) SubmoduleConfig(configs []*SubmoduleConfig) *SubmoduleConfig {
for _, config := range configs {
if f.Name == config.Name {
return config
}
}
return nil
}

View File

@@ -1,20 +0,0 @@
package models
// Remote : A git remote
type Remote struct {
Name string
Urls []string
Branches []*RemoteBranch
}
func (r *Remote) RefName() string {
return r.Name
}
func (r *Remote) ID() string {
return r.RefName()
}
func (r *Remote) Description() string {
return r.RefName()
}

View File

@@ -1,23 +0,0 @@
package models
// Remote Branch : A git remote branch
type RemoteBranch struct {
Name string
RemoteName string
}
func (r *RemoteBranch) FullName() string {
return r.RemoteName + "/" + r.Name
}
func (r *RemoteBranch) RefName() string {
return r.FullName()
}
func (r *RemoteBranch) ID() string {
return r.RefName()
}
func (r *RemoteBranch) Description() string {
return r.RefName()
}

View File

@@ -1,21 +0,0 @@
package models
import "fmt"
// StashEntry : A git stash entry
type StashEntry struct {
Index int
Name string
}
func (s *StashEntry) RefName() string {
return fmt.Sprintf("stash@{%d}", s.Index)
}
func (s *StashEntry) ID() string {
return s.RefName()
}
func (s *StashEntry) Description() string {
return s.RefName() + ": " + s.Name
}

View File

@@ -1,19 +0,0 @@
package models
type SubmoduleConfig struct {
Name string
Path string
Url string
}
func (r *SubmoduleConfig) RefName() string {
return r.Name
}
func (r *SubmoduleConfig) ID() string {
return r.RefName()
}
func (r *SubmoduleConfig) Description() string {
return r.RefName()
}

View File

@@ -1,18 +0,0 @@
package models
// Tag : A git tag
type Tag struct {
Name string
}
func (t *Tag) RefName() string {
return t.Name
}
func (t *Tag) ID() string {
return t.RefName()
}
func (t *Tag) Description() string {
return "tag " + t.Name
}

View File

@@ -1,20 +1,17 @@
package oscommands
package commands
import (
"bufio"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"github.com/go-errors/errors"
"github.com/atotto/clipboard"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/mgutz/str"
@@ -24,14 +21,13 @@ import (
// Platform stores the os state
type Platform struct {
OS string
CatCmd string
Shell string
ShellArg string
EscapedQuote string
OpenCommand string
OpenLinkCommand string
FallbackEscapedQuote string
os string
shell string
shellArg string
escapedQuote string
openCommand string
openLinkCommand string
fallbackEscapedQuote string
}
// OSCommand holds all the os commands
@@ -39,10 +35,10 @@ type OSCommand struct {
Log *logrus.Entry
Platform *Platform
Config config.AppConfigurer
Command func(string, ...string) *exec.Cmd
BeforeExecuteCmd func(*exec.Cmd)
GetGlobalGitConfig func(string) (string, error)
Getenv func(string) string
command func(string, ...string) *exec.Cmd
beforeExecuteCmd func(*exec.Cmd)
getGlobalGitConfig func(string) (string, error)
getenv func(string) string
}
// NewOSCommand os command runner
@@ -51,37 +47,21 @@ func NewOSCommand(log *logrus.Entry, config config.AppConfigurer) *OSCommand {
Log: log,
Platform: getPlatform(),
Config: config,
Command: exec.Command,
BeforeExecuteCmd: func(*exec.Cmd) {},
GetGlobalGitConfig: gitconfig.Global,
Getenv: os.Getenv,
command: exec.Command,
beforeExecuteCmd: func(*exec.Cmd) {},
getGlobalGitConfig: gitconfig.Global,
getenv: os.Getenv,
}
}
// SetCommand sets the command function used by the struct.
// To be used for testing only
func (c *OSCommand) SetCommand(cmd func(string, ...string) *exec.Cmd) {
c.Command = cmd
c.command = cmd
}
func (c *OSCommand) SetBeforeExecuteCmd(cmd func(*exec.Cmd)) {
c.BeforeExecuteCmd = cmd
}
type RunCommandOptions struct {
EnvVars []string
}
func (c *OSCommand) RunCommandWithOutputWithOptions(command string, options RunCommandOptions) (string, error) {
c.Log.WithField("command", command).Info("RunCommand")
cmd := c.ExecutableFromString(command)
cmd.Env = append(cmd.Env, options.EnvVars...)
return sanitisedCommandOutput(cmd.CombinedOutput())
}
func (c *OSCommand) RunCommandWithOptions(command string, options RunCommandOptions) error {
_, err := c.RunCommandWithOutputWithOptions(command, options)
return err
c.beforeExecuteCmd = cmd
}
// RunCommandWithOutput wrapper around commands returning their output and error
@@ -97,16 +77,12 @@ func (c *OSCommand) RunCommandWithOutput(formatString string, formatArgs ...inte
}
c.Log.WithField("command", command).Info("RunCommand")
cmd := c.ExecutableFromString(command)
output, err := sanitisedCommandOutput(cmd.CombinedOutput())
if err != nil {
c.Log.WithField("command", command).Error(err)
}
return output, err
return sanitisedCommandOutput(cmd.CombinedOutput())
}
// RunExecutableWithOutput runs an executable file and returns its output
func (c *OSCommand) RunExecutableWithOutput(cmd *exec.Cmd) (string, error) {
c.BeforeExecuteCmd(cmd)
c.beforeExecuteCmd(cmd)
return sanitisedCommandOutput(cmd.CombinedOutput())
}
@@ -119,49 +95,33 @@ func (c *OSCommand) RunExecutable(cmd *exec.Cmd) error {
// ExecutableFromString takes a string like `git status` and returns an executable command for it
func (c *OSCommand) ExecutableFromString(commandStr string) *exec.Cmd {
splitCmd := str.ToArgv(commandStr)
cmd := c.Command(splitCmd[0], splitCmd[1:]...)
cmd := c.command(splitCmd[0], splitCmd[1:]...)
cmd.Env = append(os.Environ(), "GIT_OPTIONAL_LOCKS=0")
return cmd
}
// ShellCommandFromString takes a string like `git commit` and returns an executable shell command for it
func (c *OSCommand) ShellCommandFromString(commandStr string) *exec.Cmd {
quotedCommand := ""
// Windows does not seem to like quotes around the command
if c.Platform.OS == "windows" {
quotedCommand = commandStr
} else {
quotedCommand = strconv.Quote(commandStr)
}
shellCommand := fmt.Sprintf("%s %s %s", c.Platform.Shell, c.Platform.ShellArg, quotedCommand)
return c.ExecutableFromString(shellCommand)
}
// RunCommandWithOutputLive runs RunCommandWithOutputLiveWrapper
func (c *OSCommand) RunCommandWithOutputLive(command string, output func(string) string) error {
return RunCommandWithOutputLiveWrapper(c, command, output)
}
// DetectUnamePass detect a username / password / passphrase question in a command
// promptUserForCredential is a function that gets executed when this function detect you need to fillin a password or passphrase
// The promptUserForCredential argument will be "username", "password" or "passphrase" and expects the user's password/passphrase or username back
func (c *OSCommand) DetectUnamePass(command string, promptUserForCredential func(string) string) error {
// DetectUnamePass detect a username / password question in a command
// ask is a function that gets executen when this function detect you need to fillin a password
// The ask argument will be "username" or "password" and expects the user's password or username back
func (c *OSCommand) DetectUnamePass(command string, ask func(string) string) error {
ttyText := ""
errMessage := c.RunCommandWithOutputLive(command, func(word string) string {
ttyText = ttyText + " " + word
prompts := map[string]string{
`.+'s password:`: "password",
`Password\s*for\s*'.+':`: "password",
`Username\s*for\s*'.+':`: "username",
`Enter\s*passphrase\s*for\s*key\s*'.+':`: "passphrase",
"password": `Password\s*for\s*'.+':`,
"username": `Username\s*for\s*'.+':`,
}
for pattern, askFor := range prompts {
for askFor, pattern := range prompts {
if match, _ := regexp.MatchString(pattern, ttyText); match {
ttyText = ""
return promptUserForCredential(askFor)
return ask(askFor)
}
}
@@ -193,7 +153,7 @@ func (c *OSCommand) RunDirectCommand(command string) (string, error) {
c.Log.WithField("command", command).Info("RunDirectCommand")
return sanitisedCommandOutput(
c.Command(c.Platform.Shell, c.Platform.ShellArg, command).
c.command(c.Platform.shell, c.Platform.shellArg, command).
CombinedOutput(),
)
}
@@ -204,7 +164,7 @@ func sanitisedCommandOutput(output []byte, err error) (string, error) {
// errors like 'exit status 1' are not very useful so we'll create an error
// from the combined output
if outputString == "" {
return "", utils.WrapError(err)
return "", WrapError(err)
}
return outputString, errors.New(outputString)
}
@@ -213,7 +173,7 @@ func sanitisedCommandOutput(output []byte, err error) (string, error) {
// OpenFile opens a file with the given
func (c *OSCommand) OpenFile(filename string) error {
commandTemplate := c.Config.GetUserConfig().OS.OpenCommand
commandTemplate := c.Config.GetUserConfig().GetString("os.openCommand")
templateValues := map[string]string{
"filename": c.Quote(filename),
}
@@ -225,7 +185,7 @@ func (c *OSCommand) OpenFile(filename string) error {
// OpenLink opens a file with the given
func (c *OSCommand) OpenLink(link string) error {
commandTemplate := c.Config.GetUserConfig().OS.OpenLinkCommand
commandTemplate := c.Config.GetUserConfig().GetString("os.openLinkCommand")
templateValues := map[string]string{
"link": c.Quote(link),
}
@@ -238,13 +198,13 @@ func (c *OSCommand) OpenLink(link string) error {
// EditFile opens a file in a subprocess using whatever editor is available,
// falling back to core.editor, VISUAL, EDITOR, then vi
func (c *OSCommand) EditFile(filename string) (*exec.Cmd, error) {
editor, _ := c.GetGlobalGitConfig("core.editor")
editor, _ := c.getGlobalGitConfig("core.editor")
if editor == "" {
editor = c.Getenv("VISUAL")
editor = c.getenv("VISUAL")
}
if editor == "" {
editor = c.Getenv("EDITOR")
editor = c.getenv("EDITOR")
}
if editor == "" {
if err := c.RunCommand("which vi"); err == nil {
@@ -255,15 +215,13 @@ func (c *OSCommand) EditFile(filename string) (*exec.Cmd, error) {
return nil, errors.New("No editor defined in $VISUAL, $EDITOR, or git config")
}
splitCmd := str.ToArgv(fmt.Sprintf("%s %s", editor, c.Quote(filename)))
return c.PrepareSubProcess(splitCmd[0], splitCmd[1:]...), nil
return c.PrepareSubProcess(editor, filename), nil
}
// PrepareSubProcess iniPrepareSubProcessrocess then tells the Gui to switch to it
// TODO: see if this needs to exist, given that ExecutableFromString does the same things
func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *exec.Cmd {
cmd := c.Command(cmdName, commandArgs...)
cmd := c.command(cmdName, commandArgs...)
if cmd != nil {
cmd.Env = append(os.Environ(), "GIT_OPTIONAL_LOCKS=0")
}
@@ -273,9 +231,9 @@ func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *ex
// Quote wraps a message in platform-specific quotation marks
func (c *OSCommand) Quote(message string) string {
message = strings.Replace(message, "`", "\\`", -1)
escapedQuote := c.Platform.EscapedQuote
if strings.Contains(message, c.Platform.EscapedQuote) {
escapedQuote = c.Platform.FallbackEscapedQuote
escapedQuote := c.Platform.escapedQuote
if strings.Contains(message, c.Platform.escapedQuote) {
escapedQuote = c.Platform.fallbackEscapedQuote
}
return escapedQuote + message + escapedQuote
}
@@ -290,13 +248,13 @@ func (c *OSCommand) Unquote(message string) string {
func (c *OSCommand) AppendLineToFile(filename, line string) error {
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
return utils.WrapError(err)
return WrapError(err)
}
defer f.Close()
_, err = f.WriteString("\n" + line)
if err != nil {
return utils.WrapError(err)
return WrapError(err)
}
return nil
}
@@ -306,16 +264,16 @@ func (c *OSCommand) CreateTempFile(filename, content string) (string, error) {
tmpfile, err := ioutil.TempFile("", filename)
if err != nil {
c.Log.Error(err)
return "", utils.WrapError(err)
return "", WrapError(err)
}
if _, err := tmpfile.WriteString(content); err != nil {
c.Log.Error(err)
return "", utils.WrapError(err)
return "", WrapError(err)
}
if err := tmpfile.Close(); err != nil {
c.Log.Error(err)
return "", utils.WrapError(err)
return "", WrapError(err)
}
return tmpfile.Name(), nil
@@ -330,7 +288,7 @@ func (c *OSCommand) CreateFileWithContent(path string, content string) error {
if err := ioutil.WriteFile(path, []byte(content), 0644); err != nil {
c.Log.Error(err)
return utils.WrapError(err)
return WrapError(err)
}
return nil
@@ -339,7 +297,7 @@ func (c *OSCommand) CreateFileWithContent(path string, content string) error {
// Remove removes a file or directory at the specified path
func (c *OSCommand) Remove(filename string) error {
err := os.RemoveAll(filename)
return utils.WrapError(err)
return WrapError(err)
}
// FileExists checks whether a file exists at the specified path
@@ -357,7 +315,7 @@ func (c *OSCommand) FileExists(path string) (bool, error) {
// this is useful if you need to give your command some environment variables
// before running it
func (c *OSCommand) RunPreparedCommand(cmd *exec.Cmd) error {
c.BeforeExecuteCmd(cmd)
c.beforeExecuteCmd(cmd)
out, err := cmd.CombinedOutput()
outString := string(out)
c.Log.Info(outString)
@@ -381,7 +339,7 @@ func (c *OSCommand) GetLazygitPath() string {
// RunCustomCommand returns the pointer to a custom command
func (c *OSCommand) RunCustomCommand(command string) *exec.Cmd {
return c.PrepareSubProcess(c.Platform.Shell, c.Platform.ShellArg, command)
return c.PrepareSubProcess(c.Platform.shell, c.Platform.shellArg, command)
}
// PipeCommands runs a heap of commands and pipes their inputs/outputs together like A | B | C
@@ -412,7 +370,7 @@ func (c *OSCommand) PipeCommands(commandStrings ...string) error {
for _, cmd := range cmds {
currentCmd := cmd
go utils.Safe(func() {
go func() {
stderr, err := currentCmd.StderrPipe()
if err != nil {
c.Log.Error(err)
@@ -433,7 +391,7 @@ func (c *OSCommand) PipeCommands(commandStrings ...string) error {
}
wg.Done()
})
}()
}
wg.Wait()
@@ -443,43 +401,3 @@ func (c *OSCommand) PipeCommands(commandStrings ...string) error {
}
return nil
}
func Kill(cmd *exec.Cmd) error {
if cmd.Process == nil {
// somebody got to it before we were able to, poor bastard
return nil
}
return cmd.Process.Kill()
}
func RunLineOutputCmd(cmd *exec.Cmd, onLine func(line string) (bool, error)) error {
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
return err
}
scanner := bufio.NewScanner(stdoutPipe)
scanner.Split(bufio.ScanLines)
if err := cmd.Start(); err != nil {
return err
}
for scanner.Scan() {
line := scanner.Text()
stop, err := onLine(line)
if err != nil {
return err
}
if stop {
cmd.Process.Kill()
break
}
}
cmd.Wait()
return nil
}
func (c *OSCommand) CopyToClipboard(str string) error {
return clipboard.WriteAll(str)
}

View File

@@ -0,0 +1,19 @@
// +build !windows
package commands
import (
"runtime"
)
func getPlatform() *Platform {
return &Platform{
os: runtime.GOOS,
shell: "bash",
shellArg: "-c",
escapedQuote: "'",
openCommand: "open {{filename}}",
openLinkCommand: "open {{link}}",
fallbackEscapedQuote: "\"",
}
}

View File

@@ -1,4 +1,4 @@
package oscommands
package commands
import (
"io/ioutil"
@@ -102,8 +102,8 @@ func TestOSCommandOpenFile(t *testing.T) {
for _, s := range scenarios {
OSCmd := NewDummyOSCommand()
OSCmd.Command = s.command
OSCmd.Config.GetUserConfig().OS.OpenCommand = "open {{filename}}"
OSCmd.command = s.command
OSCmd.Config.GetUserConfig().Set("os.openCommand", "open {{filename}}")
s.test(OSCmd.OpenFile(s.filename))
}
@@ -227,35 +227,13 @@ func TestOSCommandEditFile(t *testing.T) {
assert.NoError(t, err)
},
},
{
"file/with space",
func(name string, args ...string) *exec.Cmd {
if name == "which" {
return exec.Command("echo")
}
assert.EqualValues(t, "vi", name)
assert.EqualValues(t, "file/with space", args[0])
return nil
},
func(env string) string {
return ""
},
func(cf string) (string, error) {
return "", nil
},
func(cmd *exec.Cmd, err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
OSCmd := NewDummyOSCommand()
OSCmd.Command = s.command
OSCmd.GetGlobalGitConfig = s.getGlobalGitConfig
OSCmd.Getenv = s.getenv
OSCmd.command = s.command
OSCmd.getGlobalGitConfig = s.getGlobalGitConfig
OSCmd.getenv = s.getenv
s.test(OSCmd.EditFile(s.filename))
}
@@ -267,7 +245,7 @@ func TestOSCommandQuote(t *testing.T) {
actual := osCommand.Quote("hello `test`")
expected := osCommand.Platform.EscapedQuote + "hello \\`test\\`" + osCommand.Platform.EscapedQuote
expected := osCommand.Platform.escapedQuote + "hello \\`test\\`" + osCommand.Platform.escapedQuote
assert.EqualValues(t, expected, actual)
}
@@ -276,11 +254,11 @@ func TestOSCommandQuote(t *testing.T) {
func TestOSCommandQuoteSingleQuote(t *testing.T) {
osCommand := NewDummyOSCommand()
osCommand.Platform.OS = "linux"
osCommand.Platform.os = "linux"
actual := osCommand.Quote("hello 'test'")
expected := osCommand.Platform.FallbackEscapedQuote + "hello 'test'" + osCommand.Platform.FallbackEscapedQuote
expected := osCommand.Platform.fallbackEscapedQuote + "hello 'test'" + osCommand.Platform.fallbackEscapedQuote
assert.EqualValues(t, expected, actual)
}
@@ -289,11 +267,11 @@ func TestOSCommandQuoteSingleQuote(t *testing.T) {
func TestOSCommandQuoteDoubleQuote(t *testing.T) {
osCommand := NewDummyOSCommand()
osCommand.Platform.OS = "linux"
osCommand.Platform.os = "linux"
actual := osCommand.Quote(`hello "test"`)
expected := osCommand.Platform.EscapedQuote + "hello \"test\"" + osCommand.Platform.EscapedQuote
expected := osCommand.Platform.escapedQuote + "hello \"test\"" + osCommand.Platform.escapedQuote
assert.EqualValues(t, expected, actual)
}

View File

@@ -0,0 +1,11 @@
package commands
func getPlatform() *Platform {
return &Platform{
os: "windows",
shell: "cmd",
shellArg: "/c",
escapedQuote: `\"`,
fallbackEscapedQuote: "\\'",
}
}

View File

@@ -1,137 +0,0 @@
package oscommands
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
)
/* MIT License
*
* Copyright (c) 2017 Roland Singer [roland.singer@desertbit.com]
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// CopyFile copies the contents of the file named src to the file named
// by dst. The file will be created if it does not already exist. If the
// destination file exists, all it's contents will be replaced by the contents
// of the source file. The file mode will be copied from the source and
// the copied data is synced/flushed to stable storage.
func CopyFile(src, dst string) (err error) {
in, err := os.Open(src)
if err != nil {
return
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return
}
defer func() {
if e := out.Close(); e != nil {
err = e
}
}()
_, err = io.Copy(out, in)
if err != nil {
return
}
err = out.Sync()
if err != nil {
return
}
si, err := os.Stat(src)
if err != nil {
return
}
err = os.Chmod(dst, si.Mode())
if err != nil {
return
}
return
}
// CopyDir recursively copies a directory tree, attempting to preserve permissions.
// Source directory must exist. If destination already exists we'll clobber it.
// Symlinks are ignored and skipped.
func CopyDir(src string, dst string) (err error) {
src = filepath.Clean(src)
dst = filepath.Clean(dst)
si, err := os.Stat(src)
if err != nil {
return err
}
if !si.IsDir() {
return fmt.Errorf("source is not a directory")
}
_, err = os.Stat(dst)
if err != nil && !os.IsNotExist(err) {
return
}
if err == nil {
// it exists so let's remove it
if err := os.RemoveAll(dst); err != nil {
return err
}
}
err = os.MkdirAll(dst, si.Mode())
if err != nil {
return
}
entries, err := ioutil.ReadDir(src)
if err != nil {
return
}
for _, entry := range entries {
srcPath := filepath.Join(src, entry.Name())
dstPath := filepath.Join(dst, entry.Name())
if entry.IsDir() {
err = CopyDir(srcPath, dstPath)
if err != nil {
return
}
} else {
// Skip symlinks.
if entry.Mode()&os.ModeSymlink != 0 {
continue
}
err = CopyFile(srcPath, dstPath)
if err != nil {
return
}
}
}
return
}

View File

@@ -1,11 +0,0 @@
package oscommands
import (
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// NewDummyOSCommand creates a new dummy OSCommand for testing
func NewDummyOSCommand() *OSCommand {
return NewOSCommand(utils.NewDummyLog(), config.NewDummyAppConfig())
}

View File

@@ -1,20 +0,0 @@
// +build !windows
package oscommands
import (
"runtime"
)
func getPlatform() *Platform {
return &Platform{
OS: runtime.GOOS,
CatCmd: "cat",
Shell: "bash",
ShellArg: "-c",
EscapedQuote: "'",
OpenCommand: "open {{filename}}",
OpenLinkCommand: "open {{link}}",
FallbackEscapedQuote: "\"",
}
}

View File

@@ -1,12 +0,0 @@
package oscommands
func getPlatform() *Platform {
return &Platform{
OS: "windows",
CatCmd: "type",
Shell: "cmd",
ShellArg: "/c",
EscapedQuote: `\"`,
FallbackEscapedQuote: "\\'",
}
}

View File

@@ -1,142 +0,0 @@
package patch
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type PatchHunk struct {
FirstLineIdx int
oldStart int
newStart int
heading string
bodyLines []string
}
func (hunk *PatchHunk) LastLineIdx() int {
return hunk.FirstLineIdx + len(hunk.bodyLines)
}
func newHunk(lines []string, firstLineIdx int) *PatchHunk {
header := lines[0]
bodyLines := lines[1:]
oldStart, newStart, heading := headerInfo(header)
return &PatchHunk{
oldStart: oldStart,
newStart: newStart,
heading: heading,
FirstLineIdx: firstLineIdx,
bodyLines: bodyLines,
}
}
func headerInfo(header string) (int, int, string) {
match := hunkHeaderRegexp.FindStringSubmatch(header)
oldStart := utils.MustConvertToInt(match[1])
newStart := utils.MustConvertToInt(match[2])
heading := match[3]
return oldStart, newStart, heading
}
func (hunk *PatchHunk) updatedLines(lineIndices []int, reverse bool) []string {
skippedNewlineMessageIndex := -1
newLines := []string{}
lineIdx := hunk.FirstLineIdx
for _, line := range hunk.bodyLines {
lineIdx++ // incrementing at the start to skip the header line
if line == "" {
break
}
isLineSelected := utils.IncludesInt(lineIndices, lineIdx)
firstChar, content := line[:1], line[1:]
transformedFirstChar := transformedFirstChar(firstChar, reverse, isLineSelected)
if isLineSelected || (transformedFirstChar == "\\" && skippedNewlineMessageIndex != lineIdx) || transformedFirstChar == " " {
newLines = append(newLines, transformedFirstChar+content)
continue
}
if transformedFirstChar == "+" {
// we don't want to include the 'newline at end of file' line if it involves an addition we're not including
skippedNewlineMessageIndex = lineIdx + 1
}
}
return newLines
}
func transformedFirstChar(firstChar string, reverse bool, isLineSelected bool) string {
if reverse {
if !isLineSelected && firstChar == "+" {
return " "
} else if firstChar == "-" {
return "+"
} else if firstChar == "+" {
return "-"
} else {
return firstChar
}
}
if !isLineSelected && firstChar == "-" {
return " "
}
return firstChar
}
func (hunk *PatchHunk) formatHeader(oldStart int, oldLength int, newStart int, newLength int, heading string) string {
return fmt.Sprintf("@@ -%d,%d +%d,%d @@%s\n", oldStart, oldLength, newStart, newLength, heading)
}
func (hunk *PatchHunk) formatWithChanges(lineIndices []int, reverse bool, startOffset int) (int, string) {
bodyLines := hunk.updatedLines(lineIndices, reverse)
startOffset, header, ok := hunk.updatedHeader(bodyLines, startOffset, reverse)
if !ok {
return startOffset, ""
}
return startOffset, header + strings.Join(bodyLines, "")
}
func (hunk *PatchHunk) updatedHeader(newBodyLines []string, startOffset int, reverse bool) (int, string, bool) {
changeCount := nLinesWithPrefix(newBodyLines, []string{"+", "-"})
oldLength := nLinesWithPrefix(newBodyLines, []string{" ", "-"})
newLength := nLinesWithPrefix(newBodyLines, []string{"+", " "})
if changeCount == 0 {
// if nothing has changed we just return nothing
return startOffset, "", false
}
var oldStart int
if reverse {
oldStart = hunk.newStart
} else {
oldStart = hunk.oldStart
}
var newStartOffset int
// if the hunk went from zero to positive length, we need to increment the starting point by one
// if the hunk went from positive to zero length, we need to decrement the starting point by one
if oldLength == 0 {
newStartOffset = 1
} else if newLength == 0 {
newStartOffset = -1
} else {
newStartOffset = 0
}
newStart := oldStart + startOffset + newStartOffset
newStartOffset = startOffset + newLength - oldLength
formattedHeader := hunk.formatHeader(oldStart, oldLength, newStart, newLength, hunk.heading)
return newStartOffset, formattedHeader, true
}

View File

@@ -1,158 +0,0 @@
package patch
import (
"fmt"
"regexp"
"strings"
"github.com/sirupsen/logrus"
)
var hunkHeaderRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)[^\+]+\+(\d+)[^@]+@@(.*)$`)
var patchHeaderRegexp = regexp.MustCompile(`(?ms)(^diff.*?)^@@`)
func GetHeaderFromDiff(diff string) string {
match := patchHeaderRegexp.FindStringSubmatch(diff)
if len(match) <= 1 {
return ""
}
return match[1]
}
func GetHunksFromDiff(diff string) []*PatchHunk {
hunks := []*PatchHunk{}
firstLineIdx := -1
var hunkLines []string
pastDiffHeader := false
for lineIdx, line := range strings.SplitAfter(diff, "\n") {
isHunkHeader := strings.HasPrefix(line, "@@ -")
if isHunkHeader {
if pastDiffHeader { // we need to persist the current hunk
hunks = append(hunks, newHunk(hunkLines, firstLineIdx))
}
pastDiffHeader = true
firstLineIdx = lineIdx
hunkLines = []string{line}
continue
}
if !pastDiffHeader { // skip through the stuff that precedes the first hunk
continue
}
hunkLines = append(hunkLines, line)
}
if pastDiffHeader {
hunks = append(hunks, newHunk(hunkLines, firstLineIdx))
}
return hunks
}
type PatchModifier struct {
Log *logrus.Entry
filename string
hunks []*PatchHunk
header string
}
func NewPatchModifier(log *logrus.Entry, filename string, diffText string) *PatchModifier {
return &PatchModifier{
Log: log,
filename: filename,
hunks: GetHunksFromDiff(diffText),
header: GetHeaderFromDiff(diffText),
}
}
func (d *PatchModifier) ModifiedPatchForLines(lineIndices []int, reverse bool, keepOriginalHeader bool) string {
// step one is getting only those hunks which we care about
hunksInRange := []*PatchHunk{}
outer:
for _, hunk := range d.hunks {
// if there is any line in our lineIndices array that the hunk contains, we append it
for _, lineIdx := range lineIndices {
if lineIdx >= hunk.FirstLineIdx && lineIdx <= hunk.LastLineIdx() {
hunksInRange = append(hunksInRange, hunk)
continue outer
}
}
}
// step 2 is collecting all the hunks with new headers
startOffset := 0
formattedHunks := ""
var formattedHunk string
for _, hunk := range hunksInRange {
startOffset, formattedHunk = hunk.formatWithChanges(lineIndices, reverse, startOffset)
formattedHunks += formattedHunk
}
if formattedHunks == "" {
return ""
}
var fileHeader string
// for staging/unstaging lines we don't want the original header because
// it makes git confused e.g. when dealing with deleted/added files
// but with building and applying patches the original header gives git
// information it needs to cleanly apply patches
if keepOriginalHeader {
fileHeader = d.header
} else {
fileHeader = fmt.Sprintf("--- a/%s\n+++ b/%s\n", d.filename, d.filename)
}
return fileHeader + formattedHunks
}
func (d *PatchModifier) ModifiedPatchForRange(firstLineIdx int, lastLineIdx int, reverse bool, keepOriginalHeader bool) string {
// generate array of consecutive line indices from our range
selectedLines := []int{}
for i := firstLineIdx; i <= lastLineIdx; i++ {
selectedLines = append(selectedLines, i)
}
return d.ModifiedPatchForLines(selectedLines, reverse, keepOriginalHeader)
}
func (d *PatchModifier) OriginalPatchLength() int {
if len(d.hunks) == 0 {
return 0
}
return d.hunks[len(d.hunks)-1].LastLineIdx()
}
func ModifiedPatchForRange(log *logrus.Entry, filename string, diffText string, firstLineIdx int, lastLineIdx int, reverse bool, keepOriginalHeader bool) string {
p := NewPatchModifier(log, filename, diffText)
return p.ModifiedPatchForRange(firstLineIdx, lastLineIdx, reverse, keepOriginalHeader)
}
func ModifiedPatchForLines(log *logrus.Entry, filename string, diffText string, includedLineIndices []int, reverse bool, keepOriginalHeader bool) string {
p := NewPatchModifier(log, filename, diffText)
return p.ModifiedPatchForLines(includedLineIndices, reverse, keepOriginalHeader)
}
// I want to know, given a hunk, what line a given index is on
func (hunk *PatchHunk) LineNumberOfLine(idx int) int {
lines := hunk.bodyLines[0 : idx-hunk.FirstLineIdx-1]
offset := nLinesWithPrefix(lines, []string{"+", " "})
return hunk.newStart + offset
}
func nLinesWithPrefix(lines []string, chars []string) int {
result := 0
for _, line := range lines {
for _, char := range chars {
if line[:1] == char {
result++
}
}
}
return result
}

View File

@@ -1,24 +1,12 @@
package patch
package commands
import (
"errors"
"sort"
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
const (
// UNSELECTED is for when the commit file has not been added to the patch in any way
UNSELECTED = iota
// WHOLE is for when you want to add the whole diff of a file to the patch,
// including e.g. if it was deleted
WHOLE
// PART is for when you're only talking about specific lines that have been modified
PART
)
type fileInfo struct {
mode int // one of WHOLE/PART
includedLineIndices []int
@@ -26,76 +14,55 @@ type fileInfo struct {
}
type applyPatchFunc func(patch string, flags ...string) error
type loadFileDiffFunc func(from string, to string, reverse bool, filename string, plain bool) (string, error)
// PatchManager manages the building of a patch for a commit to be applied to another commit (or the working tree, or removed from the current commit). We also support building patches from things like stashes, for which there is less flexibility
// PatchManager manages the building of a patch for a commit to be applied to another commit (or the working tree, or removed from the current commit)
type PatchManager struct {
// To is the commit sha if we're dealing with files of a commit, or a stash ref for a stash
To string
From string
Reverse bool
// CanRebase tells us whether we're allowed to modify our commits. CanRebase should be true for commits of the currently checked out branch and false for everything else
// TODO: move this out into a proper mode struct in the gui package: it doesn't really belong here
CanRebase bool
// fileInfoMap starts empty but you add files to it as you go along
CommitSha string
fileInfoMap map[string]*fileInfo
Log *logrus.Entry
ApplyPatch applyPatchFunc
// LoadFileDiff loads the diff of a file, for a given to (typically a commit SHA)
LoadFileDiff loadFileDiffFunc
}
// NewPatchManager returns a new PatchManager
func NewPatchManager(log *logrus.Entry, applyPatch applyPatchFunc, loadFileDiff loadFileDiffFunc) *PatchManager {
// NewPatchManager returns a new PatchModifier
func NewPatchManager(log *logrus.Entry, applyPatch applyPatchFunc) *PatchManager {
return &PatchManager{
Log: log,
ApplyPatch: applyPatch,
LoadFileDiff: loadFileDiff,
Log: log,
ApplyPatch: applyPatch,
}
}
// NewPatchManager returns a new PatchManager
func (p *PatchManager) Start(from, to string, reverse bool, canRebase bool) {
p.To = to
p.From = from
p.Reverse = reverse
p.CanRebase = canRebase
// NewPatchManager returns a new PatchModifier
func (p *PatchManager) Start(commitSha string, diffMap map[string]string) {
p.CommitSha = commitSha
p.fileInfoMap = map[string]*fileInfo{}
}
func (p *PatchManager) addFileWhole(info *fileInfo) {
info.mode = WHOLE
lineCount := len(strings.Split(info.diff, "\n"))
info.includedLineIndices = make([]int, lineCount)
// add every line index
for i := 0; i < lineCount; i++ {
info.includedLineIndices[i] = i
for filename, diff := range diffMap {
p.fileInfoMap[filename] = &fileInfo{
mode: UNSELECTED,
diff: diff,
}
}
}
func (p *PatchManager) removeFile(info *fileInfo) {
info.mode = UNSELECTED
info.includedLineIndices = nil
func (p *PatchManager) AddFile(filename string) {
p.fileInfoMap[filename].mode = WHOLE
p.fileInfoMap[filename].includedLineIndices = nil
}
func (p *PatchManager) ToggleFileWhole(filename string) error {
info, err := p.getFileInfo(filename)
if err != nil {
return err
}
func (p *PatchManager) RemoveFile(filename string) {
p.fileInfoMap[filename].mode = UNSELECTED
p.fileInfoMap[filename].includedLineIndices = nil
}
func (p *PatchManager) ToggleFileWhole(filename string) {
info := p.fileInfoMap[filename]
switch info.mode {
case UNSELECTED, PART:
p.addFileWhole(info)
case UNSELECTED:
p.AddFile(filename)
case WHOLE:
p.removeFile(info)
default:
return errors.New("unknown file mode")
p.RemoveFile(filename)
case PART:
p.AddFile(filename)
}
return nil
}
func getIndicesForRange(first, last int) []int {
@@ -106,55 +73,24 @@ func getIndicesForRange(first, last int) []int {
return indices
}
func (p *PatchManager) getFileInfo(filename string) (*fileInfo, error) {
info, ok := p.fileInfoMap[filename]
if ok {
return info, nil
}
diff, err := p.LoadFileDiff(p.From, p.To, p.Reverse, filename, true)
if err != nil {
return nil, err
}
info = &fileInfo{
mode: UNSELECTED,
diff: diff,
}
p.fileInfoMap[filename] = info
return info, nil
}
func (p *PatchManager) AddFileLineRange(filename string, firstLineIdx, lastLineIdx int) error {
info, err := p.getFileInfo(filename)
if err != nil {
return err
}
func (p *PatchManager) AddFileLineRange(filename string, firstLineIdx, lastLineIdx int) {
info := p.fileInfoMap[filename]
info.mode = PART
info.includedLineIndices = utils.UnionInt(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx))
return nil
}
func (p *PatchManager) RemoveFileLineRange(filename string, firstLineIdx, lastLineIdx int) error {
info, err := p.getFileInfo(filename)
if err != nil {
return err
}
func (p *PatchManager) RemoveFileLineRange(filename string, firstLineIdx, lastLineIdx int) {
info := p.fileInfoMap[filename]
info.mode = PART
info.includedLineIndices = utils.DifferenceInt(info.includedLineIndices, getIndicesForRange(firstLineIdx, lastLineIdx))
if len(info.includedLineIndices) == 0 {
p.removeFile(info)
p.RemoveFile(filename)
}
return nil
}
func (p *PatchManager) renderPlainPatchForFile(filename string, reverse bool, keepOriginalHeader bool) string {
info, err := p.getFileInfo(filename)
if err != nil {
p.Log.Error(err)
func (p *PatchManager) RenderPlainPatchForFile(filename string, reverse bool, keepOriginalHeader bool) string {
info := p.fileInfoMap[filename]
if info == nil {
return ""
}
@@ -165,14 +101,15 @@ func (p *PatchManager) renderPlainPatchForFile(filename string, reverse bool, ke
return info.diff
case PART:
// generate a new diff with just the selected lines
return ModifiedPatchForLines(p.Log, filename, info.diff, info.includedLineIndices, reverse, keepOriginalHeader)
m := NewPatchModifier(p.Log, filename, info.diff)
return m.ModifiedPatchForLines(info.includedLineIndices, reverse, keepOriginalHeader)
default:
return ""
}
}
func (p *PatchManager) RenderPatchForFile(filename string, plain bool, reverse bool, keepOriginalHeader bool) string {
patch := p.renderPlainPatchForFile(filename, reverse, keepOriginalHeader)
patch := p.RenderPlainPatchForFile(filename, reverse, keepOriginalHeader)
if plain {
return patch
}
@@ -185,7 +122,7 @@ func (p *PatchManager) RenderPatchForFile(filename string, plain bool, reverse b
return parser.Render(-1, -1, nil)
}
func (p *PatchManager) renderEachFilePatch(plain bool) []string {
func (p *PatchManager) RenderEachFilePatch(plain bool) []string {
// sort files by name then iterate through and render each patch
filenames := make([]string, len(p.fileInfoMap))
index := 0
@@ -208,7 +145,7 @@ func (p *PatchManager) renderEachFilePatch(plain bool) []string {
func (p *PatchManager) RenderAggregatedPatchColored(plain bool) string {
result := ""
for _, patch := range p.renderEachFilePatch(plain) {
for _, patch := range p.RenderEachFilePatch(plain) {
if patch != "" {
result += patch + "\n"
}
@@ -217,20 +154,19 @@ func (p *PatchManager) RenderAggregatedPatchColored(plain bool) string {
}
func (p *PatchManager) GetFileStatus(filename string) int {
info, ok := p.fileInfoMap[filename]
if !ok {
info := p.fileInfoMap[filename]
if info == nil {
return UNSELECTED
}
return info.mode
}
func (p *PatchManager) GetFileIncLineIndices(filename string) ([]int, error) {
info, err := p.getFileInfo(filename)
if err != nil {
return nil, err
func (p *PatchManager) GetFileIncLineIndices(filename string) []int {
info := p.fileInfoMap[filename]
if info == nil {
return []int{}
}
return info.includedLineIndices, nil
return info.includedLineIndices
}
func (p *PatchManager) ApplyPatches(reverse bool) error {
@@ -274,12 +210,12 @@ func (p *PatchManager) ApplyPatches(reverse bool) error {
// clears the patch
func (p *PatchManager) Reset() {
p.To = ""
p.CommitSha = ""
p.fileInfoMap = map[string]*fileInfo{}
}
func (p *PatchManager) Active() bool {
return p.To != ""
func (p *PatchManager) CommitSelected() bool {
return p.CommitSha != ""
}
func (p *PatchManager) IsEmpty() bool {
@@ -291,8 +227,3 @@ func (p *PatchManager) IsEmpty() bool {
return true
}
// if any of these things change we'll need to reset and start a new patch
func (p *PatchManager) NewPatchRequired(from string, to string, reverse bool) bool {
return from != p.From || to != p.To || reverse != p.Reverse
}

View File

@@ -0,0 +1,260 @@
package commands
import (
"fmt"
"regexp"
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
var hunkHeaderRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)[^\+]+\+(\d+)[^@]+@@(.*)$`)
var patchHeaderRegexp = regexp.MustCompile(`(?ms)(^diff.*?)^@@`)
type PatchHunk struct {
header string
FirstLineIdx int
LastLineIdx int
bodyLines []string
}
func newHunk(header string, body string, firstLineIdx int) *PatchHunk {
bodyLines := strings.SplitAfter(header+body, "\n")[1:] // dropping the header line
return &PatchHunk{
header: header,
FirstLineIdx: firstLineIdx,
LastLineIdx: firstLineIdx + len(bodyLines),
bodyLines: bodyLines,
}
}
func (hunk *PatchHunk) updatedLines(lineIndices []int, reverse bool) []string {
skippedNewlineMessageIndex := -1
newLines := []string{}
lineIdx := hunk.FirstLineIdx
for _, line := range hunk.bodyLines {
lineIdx++ // incrementing at the start to skip the header line
if line == "" {
break
}
isLineSelected := utils.IncludesInt(lineIndices, lineIdx)
firstChar, content := line[:1], line[1:]
transformedFirstChar := transformedFirstChar(firstChar, reverse, isLineSelected)
if isLineSelected || (transformedFirstChar == "\\" && skippedNewlineMessageIndex != lineIdx) || transformedFirstChar == " " {
newLines = append(newLines, transformedFirstChar+content)
continue
}
if transformedFirstChar == "+" {
// we don't want to include the 'newline at end of file' line if it involves an addition we're not including
skippedNewlineMessageIndex = lineIdx + 1
}
}
return newLines
}
func transformedFirstChar(firstChar string, reverse bool, isLineSelected bool) string {
if reverse {
if !isLineSelected && firstChar == "+" {
return " "
} else if firstChar == "-" {
return "+"
} else if firstChar == "+" {
return "-"
} else {
return firstChar
}
}
if !isLineSelected && firstChar == "-" {
return " "
}
return firstChar
}
func (hunk *PatchHunk) formatHeader(oldStart int, oldLength int, newStart int, newLength int, heading string) string {
return fmt.Sprintf("@@ -%d,%d +%d,%d @@%s\n", oldStart, oldLength, newStart, newLength, heading)
}
func (hunk *PatchHunk) formatWithChanges(lineIndices []int, reverse bool, startOffset int) (int, string) {
bodyLines := hunk.updatedLines(lineIndices, reverse)
startOffset, header, ok := hunk.updatedHeader(bodyLines, startOffset, reverse)
if !ok {
return startOffset, ""
}
return startOffset, header + strings.Join(bodyLines, "")
}
func (hunk *PatchHunk) updatedHeader(newBodyLines []string, startOffset int, reverse bool) (int, string, bool) {
changeCount := 0
oldLength := 0
newLength := 0
for _, line := range newBodyLines {
switch line[:1] {
case "+":
newLength++
changeCount++
case "-":
oldLength++
changeCount++
case " ":
oldLength++
newLength++
}
}
if changeCount == 0 {
// if nothing has changed we just return nothing
return startOffset, "", false
}
// get oldstart, newstart, and heading from header
match := hunkHeaderRegexp.FindStringSubmatch(hunk.header)
var oldStart int
if reverse {
oldStart = mustConvertToInt(match[2])
} else {
oldStart = mustConvertToInt(match[1])
}
heading := match[3]
var newStartOffset int
// if the hunk went from zero to positive length, we need to increment the starting point by one
// if the hunk went from positive to zero length, we need to decrement the starting point by one
if oldLength == 0 {
newStartOffset = 1
} else if newLength == 0 {
newStartOffset = -1
} else {
newStartOffset = 0
}
newStart := oldStart + startOffset + newStartOffset
newStartOffset = startOffset + newLength - oldLength
formattedHeader := hunk.formatHeader(oldStart, oldLength, newStart, newLength, heading)
return newStartOffset, formattedHeader, true
}
func mustConvertToInt(s string) int {
i, err := strconv.Atoi(s)
if err != nil {
panic(err)
}
return i
}
func GetHeaderFromDiff(diff string) string {
match := patchHeaderRegexp.FindStringSubmatch(diff)
if len(match) <= 1 {
return ""
}
return match[1]
}
func GetHunksFromDiff(diff string) []*PatchHunk {
headers := hunkHeaderRegexp.FindAllString(diff, -1)
bodies := hunkHeaderRegexp.Split(diff, -1)[1:] // discarding top bit
headerFirstLineIndices := []int{}
for lineIdx, line := range strings.Split(diff, "\n") {
if strings.HasPrefix(line, "@@ -") {
headerFirstLineIndices = append(headerFirstLineIndices, lineIdx)
}
}
hunks := make([]*PatchHunk, len(headers))
for index, header := range headers {
hunks[index] = newHunk(header, bodies[index], headerFirstLineIndices[index])
}
return hunks
}
type PatchModifier struct {
Log *logrus.Entry
filename string
hunks []*PatchHunk
header string
}
func NewPatchModifier(log *logrus.Entry, filename string, diffText string) *PatchModifier {
return &PatchModifier{
Log: log,
filename: filename,
hunks: GetHunksFromDiff(diffText),
header: GetHeaderFromDiff(diffText),
}
}
func (d *PatchModifier) ModifiedPatchForLines(lineIndices []int, reverse bool, keepOriginalHeader bool) string {
// step one is getting only those hunks which we care about
hunksInRange := []*PatchHunk{}
outer:
for _, hunk := range d.hunks {
// if there is any line in our lineIndices array that the hunk contains, we append it
for _, lineIdx := range lineIndices {
if lineIdx >= hunk.FirstLineIdx && lineIdx <= hunk.LastLineIdx {
hunksInRange = append(hunksInRange, hunk)
continue outer
}
}
}
// step 2 is collecting all the hunks with new headers
startOffset := 0
formattedHunks := ""
var formattedHunk string
for _, hunk := range hunksInRange {
startOffset, formattedHunk = hunk.formatWithChanges(lineIndices, reverse, startOffset)
formattedHunks += formattedHunk
}
if formattedHunks == "" {
return ""
}
var fileHeader string
// for staging/unstaging lines we don't want the original header because
// it makes git confused e.g. when dealing with deleted/added files
// but with building and applying patches the original header gives git
// information it needs to cleanly apply patches
if keepOriginalHeader {
fileHeader = d.header
} else {
fileHeader = fmt.Sprintf("--- a/%s\n+++ b/%s\n", d.filename, d.filename)
}
return fileHeader + formattedHunks
}
func (d *PatchModifier) ModifiedPatchForRange(firstLineIdx int, lastLineIdx int, reverse bool, keepOriginalHeader bool) string {
// generate array of consecutive line indices from our range
selectedLines := []int{}
for i := firstLineIdx; i <= lastLineIdx; i++ {
selectedLines = append(selectedLines, i)
}
return d.ModifiedPatchForLines(selectedLines, reverse, keepOriginalHeader)
}
func (d *PatchModifier) OriginalPatchLength() int {
if len(d.hunks) == 0 {
return 0
}
return d.hunks[len(d.hunks)-1].LastLineIdx
}
func ModifiedPatchForRange(log *logrus.Entry, filename string, diffText string, firstLineIdx int, lastLineIdx int, reverse bool, keepOriginalHeader bool) string {
p := NewPatchModifier(log, filename, diffText)
return p.ModifiedPatchForRange(firstLineIdx, lastLineIdx, reverse, keepOriginalHeader)
}

View File

@@ -1,8 +1,7 @@
package patch
package commands
import (
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
@@ -89,15 +88,6 @@ index e69de29..c6568ea 100644
\ No newline at end of file
`
const exampleHunk = `@@ -1,5 +1,5 @@
apple
-grape
+orange
...
...
...
`
// TestModifyPatchForRange is a function.
func TestModifyPatchForRange(t *testing.T) {
type scenario struct {
@@ -519,30 +509,3 @@ func TestModifyPatchForRange(t *testing.T) {
})
}
}
func TestLineNumberOfLine(t *testing.T) {
type scenario struct {
testName string
hunk *PatchHunk
idx int
expected int
}
scenarios := []scenario{
{
testName: "nothing selected",
hunk: newHunk(strings.SplitAfter(exampleHunk, "\n"), 10),
idx: 15,
expected: 3,
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
result := s.hunk.LineNumberOfLine(s.idx)
if !assert.Equal(t, s.expected, result) {
fmt.Println(result)
}
})
}
}

View File

@@ -1,4 +1,4 @@
package patch
package commands
import (
"regexp"
@@ -63,7 +63,7 @@ func (p *PatchParser) GetHunkContainingLine(lineIndex int, offset int) *PatchHun
}
for index, hunk := range p.PatchHunks {
if lineIndex >= hunk.FirstLineIdx && lineIndex <= hunk.LastLineIdx() {
if lineIndex >= hunk.FirstLineIdx && lineIndex <= hunk.LastLineIdx {
resultIndex := index + offset
if resultIndex < 0 {
resultIndex = 0
@@ -75,7 +75,7 @@ func (p *PatchParser) GetHunkContainingLine(lineIndex int, offset int) *PatchHun
}
// if your cursor is past the last hunk, select the last hunk
if lineIndex > p.PatchHunks[len(p.PatchHunks)-1].LastLineIdx() {
if lineIndex > p.PatchHunks[len(p.PatchHunks)-1].LastLineIdx {
return p.PatchHunks[len(p.PatchHunks)-1]
}
@@ -120,7 +120,7 @@ func coloredString(colorAttr color.Attribute, str string, selected bool, include
var cl *color.Color
attributes := []color.Attribute{colorAttr}
if selected {
attributes = append(attributes, theme.SelectedRangeBgColor)
attributes = append(attributes, theme.SelectedLineBgColor)
}
cl = color.New(attributes...)
var clIncluded *color.Color

View File

@@ -1,22 +1,16 @@
package commands
import (
"fmt"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
)
import "github.com/go-errors/errors"
// DeletePatchesFromCommit applies a patch in reverse for a commit
func (c *GitCommand) DeletePatchesFromCommit(commits []*models.Commit, commitIndex int, p *patch.PatchManager) error {
func (c *GitCommand) DeletePatchesFromCommit(commits []*Commit, commitIndex int, p *PatchManager) error {
if err := c.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil {
return err
}
// apply each patch in reverse
if err := p.ApplyPatches(true); err != nil {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
}
return err
@@ -33,10 +27,10 @@ func (c *GitCommand) DeletePatchesFromCommit(commits []*models.Commit, commitInd
}
// continue
return c.GenericMergeOrRebaseAction("rebase", "continue")
return c.GenericMerge("rebase", "continue")
}
func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceCommitIdx int, destinationCommitIdx int, p *patch.PatchManager) error {
func (c *GitCommand) MovePatchToSelectedCommit(commits []*Commit, sourceCommitIdx int, destinationCommitIdx int, p *PatchManager) error {
if sourceCommitIdx < destinationCommitIdx {
if err := c.BeginInteractiveRebaseForCommit(commits, destinationCommitIdx); err != nil {
return err
@@ -44,7 +38,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
// apply each patch forward
if err := p.ApplyPatches(false); err != nil {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
}
return err
@@ -61,7 +55,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
}
// continue
return c.GenericMergeOrRebaseAction("rebase", "continue")
return c.GenericMerge("rebase", "continue")
}
if len(commits)-1 < sourceCommitIdx {
@@ -72,7 +66,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
// one where we handle the possibility of a credential request, and the other
// where we continue the rebase
if c.usingGpg() {
return errors.New(c.Tr.DisabledForGPG)
return errors.New(c.Tr.SLocalize("DisabledForGPG"))
}
baseIndex := sourceCommitIdx + 1
@@ -96,7 +90,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
// apply each patch in reverse
if err := p.ApplyPatches(true); err != nil {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
}
return err
@@ -115,7 +109,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
// now we should be up to the destination, so let's apply forward these patches to that.
// ideally we would ensure we're on the right commit but I'm not sure if that check is necessary
if err := p.ApplyPatches(false); err != nil {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
}
return err
@@ -131,28 +125,20 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
return nil
}
return c.GenericMergeOrRebaseAction("rebase", "continue")
return c.GenericMerge("rebase", "continue")
}
return c.GenericMergeOrRebaseAction("rebase", "continue")
return c.GenericMerge("rebase", "continue")
}
func (c *GitCommand) PullPatchIntoIndex(commits []*models.Commit, commitIdx int, p *patch.PatchManager, stash bool) error {
if stash {
if err := c.StashSave(c.Tr.StashPrefix + commits[commitIdx].Sha); err != nil {
return err
}
}
func (c *GitCommand) PullPatchIntoIndex(commits []*Commit, commitIdx int, p *PatchManager) error {
if err := c.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil {
return err
}
if err := p.ApplyPatches(true); err != nil {
if c.WorkingTreeState() == "rebasing" {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
return err
}
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
}
return err
}
@@ -169,63 +155,15 @@ func (c *GitCommand) PullPatchIntoIndex(commits []*models.Commit, commitIdx int,
c.onSuccessfulContinue = func() error {
// add patches to index
if err := p.ApplyPatches(false); err != nil {
if c.WorkingTreeState() == "rebasing" {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
return err
}
}
return err
}
if stash {
if err := c.StashDo(0, "apply"); err != nil {
if err := c.GenericMerge("rebase", "abort"); err != nil {
return err
}
return err
}
c.PatchManager.Reset()
return nil
}
return c.GenericMergeOrRebaseAction("rebase", "continue")
}
func (c *GitCommand) PullPatchIntoNewCommit(commits []*models.Commit, commitIdx int, p *patch.PatchManager) error {
if err := c.BeginInteractiveRebaseForCommit(commits, commitIdx); err != nil {
return err
}
if err := p.ApplyPatches(true); err != nil {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
return err
}
return err
}
// amend the commit
if _, err := c.AmendHead(); err != nil {
return err
}
// add patches to index
if err := p.ApplyPatches(false); err != nil {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
return err
}
return err
}
head_message, _ := c.GetHeadCommitMessage()
new_message := fmt.Sprintf("Split from \"%s\"", head_message)
_, err := c.Commit(new_message, "")
if err != nil {
return err
}
if c.onSuccessfulContinue != nil {
return errors.New("You are midway through another rebase operation. Please abort to start again")
}
c.PatchManager.Reset()
return c.GenericMergeOrRebaseAction("rebase", "continue")
return c.GenericMerge("rebase", "continue")
}

View File

@@ -5,8 +5,6 @@ import (
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/config"
)
// Service is a service that repository is on (Github, Bitbucket, ...)
@@ -28,73 +26,37 @@ type RepoInformation struct {
Repository string
}
// NewService builds a Service based on the host type
func NewService(typeName string, repositoryDomain string, siteDomain string) *Service {
var service *Service
switch typeName {
case "github":
service = &Service{
Name: repositoryDomain,
PullRequestURL: fmt.Sprintf("https://%s%s", siteDomain, "/%s/%s/compare/%s?expand=1"),
}
case "bitbucket":
service = &Service{
Name: repositoryDomain,
PullRequestURL: fmt.Sprintf("https://%s%s", siteDomain, "/%s/%s/pull-requests/new?source=%s&t=1"),
}
case "gitlab":
service = &Service{
Name: repositoryDomain,
PullRequestURL: fmt.Sprintf("https://%s%s", siteDomain, "/%s/%s/merge_requests/new?merge_request[source_branch]=%s"),
}
func getServices() []*Service {
return []*Service{
{
Name: "github.com",
PullRequestURL: "https://github.com/%s/%s/compare/%s?expand=1",
},
{
Name: "bitbucket.org",
PullRequestURL: "https://bitbucket.org/%s/%s/pull-requests/new?source=%s&t=1",
},
{
Name: "gitlab.com",
PullRequestURL: "https://gitlab.com/%s/%s/merge_requests/new?merge_request[source_branch]=%s",
},
}
return service
}
func getServices(config config.AppConfigurer) []*Service {
services := []*Service{
NewService("github", "github.com", "github.com"),
NewService("bitbucket", "bitbucket.org", "bitbucket.org"),
NewService("gitlab", "gitlab.com", "gitlab.com"),
}
configServices := config.GetUserConfig().Services
for repoDomain, typeAndDomain := range configServices {
splitData := strings.Split(typeAndDomain, ":")
if len(splitData) != 2 {
// TODO log this misconfiguration
continue
}
service := NewService(splitData[0], repoDomain, splitData[1])
if service == nil {
// TODO log this unsupported service
continue
}
services = append(services, service)
}
return services
}
// NewPullRequest creates new instance of PullRequest
func NewPullRequest(gitCommand *GitCommand) *PullRequest {
return &PullRequest{
GitServices: getServices(gitCommand.Config),
GitServices: getServices(),
GitCommand: gitCommand,
}
}
// Create opens link to new pull request in browser
func (pr *PullRequest) Create(branch *models.Branch) error {
func (pr *PullRequest) Create(branch *Branch) error {
branchExistsOnRemote := pr.GitCommand.CheckRemoteBranchExists(branch)
if !branchExistsOnRemote {
return errors.New(pr.GitCommand.Tr.NoBranchOnRemote)
return errors.New(pr.GitCommand.Tr.SLocalize("NoBranchOnRemote"))
}
repoURL := pr.GitCommand.GetRemoteURL()
@@ -108,7 +70,7 @@ func (pr *PullRequest) Create(branch *models.Branch) error {
}
if gitService == nil {
return errors.New(pr.GitCommand.Tr.UnsupportedGitService)
return errors.New(pr.GitCommand.Tr.SLocalize("UnsupportedGitService"))
}
repoInfo := getRepoInfoFromURL(repoURL)
@@ -123,7 +85,7 @@ func getRepoInfoFromURL(url string) *RepoInformation {
if isHTTP {
splits := strings.Split(url, "/")
owner := strings.Join(splits[3:len(splits)-1], "/")
owner := splits[len(splits)-2]
repo := strings.TrimSuffix(splits[len(splits)-1], ".git")
return &RepoInformation{
@@ -134,8 +96,8 @@ func getRepoInfoFromURL(url string) *RepoInformation {
tmpSplit := strings.Split(url, ":")
splits := strings.Split(tmpSplit[1], "/")
owner := strings.Join(splits[0:len(splits)-1], "/")
repo := strings.TrimSuffix(splits[len(splits)-1], ".git")
owner := splits[0]
repo := strings.TrimSuffix(splits[1], ".git")
return &RepoInformation{
Owner: owner,

View File

@@ -5,7 +5,6 @@ import (
"strings"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/stretchr/testify/assert"
)
@@ -47,7 +46,7 @@ func TestGetRepoInfoFromURL(t *testing.T) {
func TestCreatePullRequest(t *testing.T) {
type scenario struct {
testName string
branch *models.Branch
branch *Branch
command func(string, ...string) *exec.Cmd
test func(err error)
}
@@ -55,7 +54,7 @@ func TestCreatePullRequest(t *testing.T) {
scenarios := []scenario{
{
"Opens a link to new pull request on bitbucket",
&models.Branch{
&Branch{
Name: "feature/profile-page",
},
func(cmd string, args ...string) *exec.Cmd {
@@ -74,7 +73,7 @@ func TestCreatePullRequest(t *testing.T) {
},
{
"Opens a link to new pull request on bitbucket with http remote url",
&models.Branch{
&Branch{
Name: "feature/events",
},
func(cmd string, args ...string) *exec.Cmd {
@@ -93,7 +92,7 @@ func TestCreatePullRequest(t *testing.T) {
},
{
"Opens a link to new pull request on github",
&models.Branch{
&Branch{
Name: "feature/sum-operation",
},
func(cmd string, args ...string) *exec.Cmd {
@@ -112,7 +111,7 @@ func TestCreatePullRequest(t *testing.T) {
},
{
"Opens a link to new pull request on gitlab",
&models.Branch{
&Branch{
Name: "feature/ui",
},
func(cmd string, args ...string) *exec.Cmd {
@@ -131,7 +130,7 @@ func TestCreatePullRequest(t *testing.T) {
},
{
"Throws an error if git service is unsupported",
&models.Branch{
&Branch{
Name: "feature/divide-operation",
},
func(cmd string, args ...string) *exec.Cmd {
@@ -146,15 +145,8 @@ func TestCreatePullRequest(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCommand := NewDummyGitCommand()
gitCommand.OSCommand.Command = s.command
gitCommand.OSCommand.Config.GetUserConfig().OS.OpenLinkCommand = "open {{link}}"
gitCommand.OSCommand.Config.GetUserConfig().Services = map[string]string{
// valid configuration for a custom service URL
"git.work.com": "gitlab:code.work.com",
// invalid configurations for a custom service URL
"invalid.work.com": "noservice:invalid.work.com",
"noservice.work.com": "noservice.work.com",
}
gitCommand.OSCommand.command = s.command
gitCommand.OSCommand.Config.GetUserConfig().Set("os.openLinkCommand", "open {{link}}")
dummyPullRequest := NewPullRequest(gitCommand)
s.test(dummyPullRequest.Create(s.branch))
})

View File

@@ -1,287 +0,0 @@
package commands
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/mgutz/str"
)
func (c *GitCommand) RewordCommit(commits []*models.Commit, index int) (*exec.Cmd, error) {
todo, sha, err := c.GenerateGenericRebaseTodo(commits, index, "reword")
if err != nil {
return nil, err
}
return c.PrepareInteractiveRebaseCommand(sha, todo, false)
}
func (c *GitCommand) MoveCommitDown(commits []*models.Commit, index int) error {
// we must ensure that we have at least two commits after the selected one
if len(commits) <= index+2 {
// assuming they aren't picking the bottom commit
return errors.New(c.Tr.NoRoom)
}
todo := ""
orderedCommits := append(commits[0:index], commits[index+1], commits[index])
for _, commit := range orderedCommits {
todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo
}
cmd, err := c.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todo, true)
if err != nil {
return err
}
return c.OSCommand.RunPreparedCommand(cmd)
}
func (c *GitCommand) InteractiveRebase(commits []*models.Commit, index int, action string) error {
todo, sha, err := c.GenerateGenericRebaseTodo(commits, index, action)
if err != nil {
return err
}
cmd, err := c.PrepareInteractiveRebaseCommand(sha, todo, true)
if err != nil {
return err
}
return c.OSCommand.RunPreparedCommand(cmd)
}
// PrepareInteractiveRebaseCommand returns the cmd for an interactive rebase
// we tell git to run lazygit to edit the todo list, and we pass the client
// lazygit a todo string to write to the todo file
func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string, overrideEditor bool) (*exec.Cmd, error) {
ex := c.OSCommand.GetLazygitPath()
debug := "FALSE"
if c.OSCommand.Config.GetDebug() {
debug = "TRUE"
}
cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty %s", baseSha)
c.Log.WithField("command", cmdStr).Info("RunCommand")
splitCmd := str.ToArgv(cmdStr)
cmd := c.OSCommand.Command(splitCmd[0], splitCmd[1:]...)
gitSequenceEditor := ex
if todo == "" {
gitSequenceEditor = "true"
}
cmd.Env = os.Environ()
cmd.Env = append(
cmd.Env,
"LAZYGIT_CLIENT_COMMAND=INTERACTIVE_REBASE",
"LAZYGIT_REBASE_TODO="+todo,
"DEBUG="+debug,
"LANG=en_US.UTF-8", // Force using EN as language
"LC_ALL=en_US.UTF-8", // Force using EN as language
"GIT_SEQUENCE_EDITOR="+gitSequenceEditor,
)
if overrideEditor {
cmd.Env = append(cmd.Env, "GIT_EDITOR="+ex)
}
return cmd, nil
}
func (c *GitCommand) GenerateGenericRebaseTodo(commits []*models.Commit, actionIndex int, action string) (string, string, error) {
baseIndex := actionIndex + 1
if len(commits) <= baseIndex {
return "", "", errors.New(c.Tr.CannotRebaseOntoFirstCommit)
}
if action == "squash" || action == "fixup" {
baseIndex++
if len(commits) <= baseIndex {
return "", "", errors.New(c.Tr.CannotSquashOntoSecondCommit)
}
}
todo := ""
for i, commit := range commits[0:baseIndex] {
var commitAction string
if i == actionIndex {
commitAction = action
} else if commit.IsMerge {
// your typical interactive rebase will actually drop merge commits by default. Damn git CLI, you scary!
// doing this means we don't need to worry about rebasing over merges which always causes problems.
// you typically shouldn't be doing rebases that pass over merge commits anyway.
commitAction = "drop"
} else {
commitAction = "pick"
}
todo = commitAction + " " + commit.Sha + " " + commit.Name + "\n" + todo
}
return todo, commits[baseIndex].Sha, nil
}
// AmendTo amends the given commit with whatever files are staged
func (c *GitCommand) AmendTo(sha string) error {
if err := c.CreateFixupCommit(sha); err != nil {
return err
}
return c.SquashAllAboveFixupCommits(sha)
}
// EditRebaseTodo sets the action at a given index in the git-rebase-todo file
func (c *GitCommand) EditRebaseTodo(index int, action string) error {
fileName := filepath.Join(c.DotGitDir, "rebase-merge/git-rebase-todo")
bytes, err := ioutil.ReadFile(fileName)
if err != nil {
return err
}
content := strings.Split(string(bytes), "\n")
commitCount := c.getTodoCommitCount(content)
// we have the most recent commit at the bottom whereas the todo file has
// it at the bottom, so we need to subtract our index from the commit count
contentIndex := commitCount - 1 - index
splitLine := strings.Split(content[contentIndex], " ")
content[contentIndex] = action + " " + strings.Join(splitLine[1:], " ")
result := strings.Join(content, "\n")
return ioutil.WriteFile(fileName, []byte(result), 0644)
}
func (c *GitCommand) getTodoCommitCount(content []string) int {
// count lines that are not blank and are not comments
commitCount := 0
for _, line := range content {
if line != "" && !strings.HasPrefix(line, "#") {
commitCount++
}
}
return commitCount
}
// MoveTodoDown moves a rebase todo item down by one position
func (c *GitCommand) MoveTodoDown(index int) error {
fileName := filepath.Join(c.DotGitDir, "rebase-merge/git-rebase-todo")
bytes, err := ioutil.ReadFile(fileName)
if err != nil {
return err
}
content := strings.Split(string(bytes), "\n")
commitCount := c.getTodoCommitCount(content)
contentIndex := commitCount - 1 - index
rearrangedContent := append(content[0:contentIndex-1], content[contentIndex], content[contentIndex-1])
rearrangedContent = append(rearrangedContent, content[contentIndex+1:]...)
result := strings.Join(rearrangedContent, "\n")
return ioutil.WriteFile(fileName, []byte(result), 0644)
}
// SquashAllAboveFixupCommits squashes all fixup! commits above the given one
func (c *GitCommand) SquashAllAboveFixupCommits(sha string) error {
return c.runSkipEditorCommand(
fmt.Sprintf(
"git rebase --interactive --autostash --autosquash %s^",
sha,
),
)
}
// BeginInteractiveRebaseForCommit starts an interactive rebase to edit the current
// commit and pick all others. After this you'll want to call `c.GenericMergeOrRebaseAction("rebase", "continue")`
func (c *GitCommand) BeginInteractiveRebaseForCommit(commits []*models.Commit, commitIndex int) error {
if len(commits)-1 < commitIndex {
return errors.New("index outside of range of commits")
}
// we can make this GPG thing possible it just means we need to do this in two parts:
// one where we handle the possibility of a credential request, and the other
// where we continue the rebase
if c.usingGpg() {
return errors.New(c.Tr.DisabledForGPG)
}
todo, sha, err := c.GenerateGenericRebaseTodo(commits, commitIndex, "edit")
if err != nil {
return err
}
cmd, err := c.PrepareInteractiveRebaseCommand(sha, todo, true)
if err != nil {
return err
}
if err := c.OSCommand.RunPreparedCommand(cmd); err != nil {
return err
}
return nil
}
// RebaseBranch interactive rebases onto a branch
func (c *GitCommand) RebaseBranch(branchName string) error {
cmd, err := c.PrepareInteractiveRebaseCommand(branchName, "", false)
if err != nil {
return err
}
return c.OSCommand.RunPreparedCommand(cmd)
}
// GenericMerge takes a commandType of "merge" or "rebase" and a command of "abort", "skip" or "continue"
// By default we skip the editor in the case where a commit will be made
func (c *GitCommand) GenericMergeOrRebaseAction(commandType string, command string) error {
err := c.runSkipEditorCommand(
fmt.Sprintf(
"git %s --%s",
commandType,
command,
),
)
if err != nil {
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
// fix merge conflicts along the way. When this happens we queue up the next step
// so that after the next successful rebase continue we can continue from where we left off
if commandType == "rebase" && command == "continue" && c.onSuccessfulContinue != nil {
f := c.onSuccessfulContinue
c.onSuccessfulContinue = nil
return f()
}
if command == "abort" {
c.onSuccessfulContinue = nil
}
return nil
}
func (c *GitCommand) runSkipEditorCommand(command string) error {
cmd := c.OSCommand.ExecutableFromString(command)
lazyGitPath := c.OSCommand.GetLazygitPath()
cmd.Env = append(
cmd.Env,
"LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY",
"GIT_EDITOR="+lazyGitPath,
"EDITOR="+lazyGitPath,
"VISUAL="+lazyGitPath,
)
return c.OSCommand.RunExecutable(cmd)
}

24
pkg/commands/remote.go Normal file
View File

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

@@ -0,0 +1,19 @@
package commands
import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
// Remote Branch : A git remote branch
type RemoteBranch struct {
Name string
Selected bool
RemoteName string
}
// GetDisplayStrings returns the display string of branch
func (b *RemoteBranch) GetDisplayStrings(isFocused bool) []string {
displayName := utils.ColoredString(b.Name, GetBranchColor(b.Name))
return []string{displayName}
}

View File

@@ -1,43 +0,0 @@
package commands
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/commands/models"
)
func (c *GitCommand) AddRemote(name string, url string) error {
return c.OSCommand.RunCommand("git remote add %s %s", name, url)
}
func (c *GitCommand) RemoveRemote(name string) error {
return c.OSCommand.RunCommand("git remote remove %s", name)
}
func (c *GitCommand) RenameRemote(oldRemoteName string, newRemoteName string) error {
return c.OSCommand.RunCommand("git remote rename %s %s", oldRemoteName, newRemoteName)
}
func (c *GitCommand) UpdateRemoteUrl(remoteName string, updatedUrl string) error {
return c.OSCommand.RunCommand("git remote set-url %s %s", remoteName, updatedUrl)
}
func (c *GitCommand) DeleteRemoteBranch(remoteName string, branchName string, promptUserForCredential func(string) string) error {
command := fmt.Sprintf("git push %s --delete %s", remoteName, branchName)
return c.OSCommand.DetectUnamePass(command, promptUserForCredential)
}
// CheckRemoteBranchExists Returns remote branch
func (c *GitCommand) CheckRemoteBranchExists(branch *models.Branch) bool {
_, err := c.OSCommand.RunCommandWithOutput(
"git show-ref --verify -- refs/remotes/origin/%s",
branch.Name,
)
return err == nil
}
// GetRemoteURL returns current repo remote url
func (c *GitCommand) GetRemoteURL() string {
return c.GetConfigValue("remote.origin.url")
}

View File

@@ -1,58 +0,0 @@
package commands
import "fmt"
// StashDo modify stash
func (c *GitCommand) StashDo(index int, method string) error {
return c.OSCommand.RunCommand("git stash %s stash@{%d}", method, index)
}
// StashSave save stash
// TODO: before calling this, check if there is anything to save
func (c *GitCommand) StashSave(message string) error {
return c.OSCommand.RunCommand("git stash save %s", c.OSCommand.Quote(message))
}
// GetStashEntryDiff stash diff
func (c *GitCommand) ShowStashEntryCmdStr(index int) string {
return fmt.Sprintf("git stash show -p --stat --color=%s stash@{%d}", c.colorArg(), index)
}
// StashSaveStagedChanges stashes only the currently staged changes. This takes a few steps
// shoutouts to Joe on https://stackoverflow.com/questions/14759748/stashing-only-staged-changes-in-git-is-it-possible
func (c *GitCommand) StashSaveStagedChanges(message string) error {
if err := c.OSCommand.RunCommand("git stash --keep-index"); err != nil {
return err
}
if err := c.StashSave(message); err != nil {
return err
}
if err := c.OSCommand.RunCommand("git stash apply stash@{1}"); err != nil {
return err
}
if err := c.OSCommand.PipeCommands("git stash show -p", "git apply -R"); err != nil {
return err
}
if err := c.OSCommand.RunCommand("git stash drop stash@{1}"); err != nil {
return err
}
// if you had staged an untracked file, that will now appear as 'AD' in git status
// meaning it's deleted in your working tree but added in your index. Given that it's
// now safely stashed, we need to remove it.
files := c.GetStatusFiles(GetStatusFileOptions{})
for _, file := range files {
if file.ShortStatus == "AD" {
if err := c.UnStageFile(file.Name, false); err != nil {
return err
}
}
}
return nil
}

View File

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

View File

@@ -1,48 +0,0 @@
package commands
import (
"path/filepath"
gogit "github.com/jesseduffield/go-git/v5"
)
// RebaseMode returns "" for non-rebase mode, "normal" for normal rebase
// and "interactive" for interactive rebase
func (c *GitCommand) RebaseMode() (string, error) {
exists, err := c.OSCommand.FileExists(filepath.Join(c.DotGitDir, "rebase-apply"))
if err != nil {
return "", err
}
if exists {
return "normal", nil
}
exists, err = c.OSCommand.FileExists(filepath.Join(c.DotGitDir, "rebase-merge"))
if exists {
return "interactive", err
} else {
return "", err
}
}
func (c *GitCommand) WorkingTreeState() string {
rebaseMode, _ := c.RebaseMode()
if rebaseMode != "" {
return "rebasing"
}
merging, _ := c.IsInMergeState()
if merging {
return "merging"
}
return "normal"
}
// IsInMergeState states whether we are still mid-merge
func (c *GitCommand) IsInMergeState() (bool, error) {
return c.OSCommand.FileExists(filepath.Join(c.DotGitDir, "MERGE_HEAD"))
}
func (c *GitCommand) IsBareRepo() bool {
// note: could use `git rev-parse --is-bare-repository` if we wanna drop go-git
_, err := c.Repo.Worktree()
return err == gogit.ErrIsBareRepository
}

View File

@@ -1,165 +0,0 @@
package commands
import (
"bufio"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
)
// .gitmodules looks like this:
// [submodule "mysubmodule"]
// path = blah/mysubmodule
// url = git@github.com:subbo.git
func (c *GitCommand) GetSubmoduleConfigs() ([]*models.SubmoduleConfig, error) {
file, err := os.Open(".gitmodules")
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines)
firstMatch := func(str string, regex string) (string, bool) {
re := regexp.MustCompile(regex)
matches := re.FindStringSubmatch(str)
if len(matches) > 0 {
return matches[1], true
} else {
return "", false
}
}
configs := []*models.SubmoduleConfig{}
for scanner.Scan() {
line := scanner.Text()
if name, ok := firstMatch(line, `\[submodule "(.*)"\]`); ok {
configs = append(configs, &models.SubmoduleConfig{Name: name})
continue
}
if len(configs) > 0 {
lastConfig := configs[len(configs)-1]
if path, ok := firstMatch(line, `\s*path\s*=\s*(.*)\s*`); ok {
lastConfig.Path = path
} else if url, ok := firstMatch(line, `\s*url\s*=\s*(.*)\s*`); ok {
lastConfig.Url = url
}
}
}
return configs, nil
}
func (c *GitCommand) SubmoduleStash(submodule *models.SubmoduleConfig) error {
// if the path does not exist then it hasn't yet been initialized so we'll swallow the error
// because the intention here is to have no dirty worktree state
if _, err := os.Stat(submodule.Path); os.IsNotExist(err) {
c.Log.Infof("submodule path %s does not exist, returning", submodule.Path)
return nil
}
return c.OSCommand.RunCommand("git -C %s stash --include-untracked", submodule.Path)
}
func (c *GitCommand) SubmoduleReset(submodule *models.SubmoduleConfig) error {
return c.OSCommand.RunCommand("git submodule update --init --force %s", submodule.Path)
}
func (c *GitCommand) SubmoduleUpdateAll() error {
// not doing an --init here because the user probably doesn't want that
return c.OSCommand.RunCommand("git submodule update --force")
}
func (c *GitCommand) SubmoduleDelete(submodule *models.SubmoduleConfig) error {
// based on https://gist.github.com/myusuf3/7f645819ded92bda6677
if err := c.OSCommand.RunCommand("git submodule deinit --force %s", submodule.Path); err != nil {
if strings.Contains(err.Error(), "did not match any file(s) known to git") {
if err := c.OSCommand.RunCommand("git config --file .gitmodules --remove-section submodule.%s", submodule.Name); err != nil {
return err
}
if err := c.OSCommand.RunCommand("git config --remove-section submodule.%s", submodule.Name); err != nil {
return err
}
// if there's an error here about it not existing then we'll just continue to do `git rm`
} else {
return err
}
}
if err := c.OSCommand.RunCommand("git rm --force -r %s", submodule.Path); err != nil {
// if the directory isn't there then that's fine
c.Log.Error(err)
}
return os.RemoveAll(filepath.Join(c.DotGitDir, "modules", submodule.Path))
}
func (c *GitCommand) SubmoduleAdd(name string, path string, url string) error {
return c.OSCommand.RunCommand(
"git submodule add --force --name %s -- %s %s ",
c.OSCommand.Quote(name),
c.OSCommand.Quote(url),
c.OSCommand.Quote(path),
)
}
func (c *GitCommand) SubmoduleUpdateUrl(name string, path string, newUrl string) error {
// the set-url command is only for later git versions so we're doing it manually here
if err := c.OSCommand.RunCommand("git config --file .gitmodules submodule.%s.url %s", name, newUrl); err != nil {
return err
}
if err := c.OSCommand.RunCommand("git submodule sync %s", path); err != nil {
return err
}
return nil
}
func (c *GitCommand) SubmoduleInit(path string) error {
return c.OSCommand.RunCommand("git submodule init %s", path)
}
func (c *GitCommand) SubmoduleUpdate(path string) error {
return c.OSCommand.RunCommand("git submodule update --init %s", path)
}
func (c *GitCommand) SubmoduleBulkInitCmdStr() string {
return "git submodule init"
}
func (c *GitCommand) SubmoduleBulkUpdateCmdStr() string {
return "git submodule update"
}
func (c *GitCommand) SubmoduleForceBulkUpdateCmdStr() string {
return "git submodule update --force"
}
func (c *GitCommand) SubmoduleBulkDeinitCmdStr() string {
return "git submodule deinit --all --force"
}
func (c *GitCommand) ResetSubmodules(submodules []*models.SubmoduleConfig) error {
for _, submodule := range submodules {
if err := c.SubmoduleStash(submodule); err != nil {
return err
}
}
return c.SubmoduleUpdateAll()
}

View File

@@ -1,74 +0,0 @@
package commands
import (
"fmt"
"strings"
)
// usingGpg tells us whether the user has gpg enabled so that we can know
// whether we need to run a subprocess to allow them to enter their password
func (c *GitCommand) usingGpg() bool {
overrideGpg := c.Config.GetUserConfig().Git.OverrideGpg
if overrideGpg {
return false
}
gpgsign, _ := c.getLocalGitConfig("commit.gpgsign")
if gpgsign == "" {
gpgsign, _ = c.getGlobalGitConfig("commit.gpgsign")
}
value := strings.ToLower(gpgsign)
return value == "true" || value == "1" || value == "yes" || value == "on"
}
// Push pushes to a branch
func (c *GitCommand) Push(branchName string, force bool, upstream string, args string, promptUserForCredential func(string) string) error {
forceFlag := ""
if force {
forceFlag = "--force-with-lease"
}
setUpstreamArg := ""
if upstream != "" {
setUpstreamArg = "--set-upstream " + upstream
}
cmd := fmt.Sprintf("git push --follow-tags %s %s %s", forceFlag, setUpstreamArg, args)
return c.OSCommand.DetectUnamePass(cmd, promptUserForCredential)
}
type FetchOptions struct {
PromptUserForCredential func(string) string
RemoteName string
BranchName string
}
// Fetch fetch git repo
func (c *GitCommand) Fetch(opts FetchOptions) error {
command := "git fetch"
if opts.RemoteName != "" {
command = fmt.Sprintf("%s %s", command, opts.RemoteName)
}
if opts.BranchName != "" {
command = fmt.Sprintf("%s %s", command, opts.BranchName)
}
return c.OSCommand.DetectUnamePass(command, func(question string) string {
if opts.PromptUserForCredential != nil {
return opts.PromptUserForCredential(question)
}
return "\n"
})
}
func (c *GitCommand) FastForward(branchName string, remoteName string, remoteBranchName string, promptUserForCredential func(string) string) error {
command := fmt.Sprintf("git fetch %s %s:%s", remoteName, remoteBranchName, branchName)
return c.OSCommand.DetectUnamePass(command, promptUserForCredential)
}
func (c *GitCommand) FetchRemote(remoteName string, promptUserForCredential func(string) string) error {
command := fmt.Sprintf("git fetch %s", remoteName)
return c.OSCommand.DetectUnamePass(command, promptUserForCredential)
}

11
pkg/commands/tag.go Normal file
View File

@@ -0,0 +1,11 @@
package commands
// Tag : A git tag
type Tag struct {
Name string
}
// GetDisplayStrings returns the display string of a remote
func (r *Tag) GetDisplayStrings(isFocused bool) []string {
return []string{r.Name}
}

View File

@@ -1,16 +0,0 @@
package commands
import "fmt"
func (c *GitCommand) CreateLightweightTag(tagName string, commitSha string) error {
return c.OSCommand.RunCommand("git tag %s %s", tagName, commitSha)
}
func (c *GitCommand) DeleteTag(tagName string) error {
return c.OSCommand.RunCommand("git tag -d %s", tagName)
}
func (c *GitCommand) PushTag(remoteName string, tagName string, promptUserForCredential func(string) string) error {
command := fmt.Sprintf("git push %s %s", remoteName, tagName)
return c.OSCommand.DetectUnamePass(command, promptUserForCredential)
}

View File

@@ -1,28 +1,28 @@
package config
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/OpenPeeDeeP/xdg"
yaml "github.com/jesseduffield/yaml"
"github.com/shibukawa/configdir"
"github.com/spf13/viper"
yaml "gopkg.in/yaml.v2"
)
// AppConfig contains the base configuration fields required for lazygit.
type AppConfig struct {
Debug bool `long:"debug" env:"DEBUG" default:"false"`
Version string `long:"version" env:"VERSION" default:"unversioned"`
Commit string `long:"commit" env:"COMMIT"`
BuildDate string `long:"build-date" env:"BUILD_DATE"`
Name string `long:"name" env:"NAME" default:"lazygit"`
BuildSource string `long:"build-source" env:"BUILD_SOURCE" default:""`
UserConfig *UserConfig
UserConfigDir string
UserConfigPath string
AppState *AppState
IsNewRepo bool
Debug bool `long:"debug" env:"DEBUG" default:"false"`
Version string `long:"version" env:"VERSION" default:"unversioned"`
Commit string `long:"commit" env:"COMMIT"`
BuildDate string `long:"build-date" env:"BUILD_DATE"`
Name string `long:"name" env:"NAME" default:"lazygit"`
BuildSource string `long:"build-source" env:"BUILD_SOURCE" default:""`
UserConfig *viper.Viper
UserConfigDir string
AppState *AppState
IsNewRepo bool
}
// AppConfigurer interface allows individual app config structs to inherit Fields
@@ -34,23 +34,19 @@ type AppConfigurer interface {
GetBuildDate() string
GetName() string
GetBuildSource() string
GetUserConfig() *UserConfig
GetUserConfig() *viper.Viper
GetUserConfigDir() string
GetUserConfigPath() string
GetAppState() *AppState
WriteToUserConfig(string, interface{}) error
SaveAppState() error
LoadAppState() error
SetIsNewRepo(bool)
GetIsNewRepo() bool
}
// NewAppConfig makes a new app config
func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag bool) (*AppConfig, error) {
configDir, err := findOrCreateConfigDir()
if err != nil {
return nil, err
}
userConfig, err := loadUserConfigWithDefaults(configDir)
userConfig, userConfigPath, err := LoadConfig("config", true)
if err != nil {
return nil, err
}
@@ -59,85 +55,26 @@ func NewAppConfig(name, version, commit, date string, buildSource string, debugg
debuggingFlag = true
}
appState, err := loadAppState()
if err != nil {
return nil, err
appConfig := &AppConfig{
Name: "lazygit",
Version: version,
Commit: commit,
BuildDate: date,
Debug: debuggingFlag,
BuildSource: buildSource,
UserConfig: userConfig,
UserConfigDir: filepath.Dir(userConfigPath),
AppState: &AppState{},
IsNewRepo: false,
}
appConfig := &AppConfig{
Name: "lazygit",
Version: version,
Commit: commit,
BuildDate: date,
Debug: debuggingFlag,
BuildSource: buildSource,
UserConfig: userConfig,
UserConfigDir: configDir,
UserConfigPath: filepath.Join(configDir, "config.yml"),
AppState: appState,
IsNewRepo: false,
if err := appConfig.LoadAppState(); err != nil {
return nil, err
}
return appConfig, nil
}
func ConfigDir() string {
envConfigDir := os.Getenv("CONFIG_DIR")
if envConfigDir != "" {
return envConfigDir
}
// chucking my name there is not for vanity purposes, the xdg spec (and that
// function) requires a vendor name. May as well line up with github
configDirs := xdg.New("jesseduffield", "lazygit")
return configDirs.ConfigHome()
}
func findOrCreateConfigDir() (string, error) {
folder := ConfigDir()
err := os.MkdirAll(folder, 0755)
if err != nil {
return "", err
}
return folder, nil
}
func loadUserConfigWithDefaults(configDir string) (*UserConfig, error) {
return loadUserConfig(configDir, GetDefaultConfig())
}
func loadUserConfig(configDir string, base *UserConfig) (*UserConfig, error) {
fileName := filepath.Join(configDir, "config.yml")
if _, err := os.Stat(fileName); err != nil {
if os.IsNotExist(err) {
file, err := os.Create(fileName)
if err != nil {
if strings.Contains(err.Error(), "read-only file system") {
return base, nil
}
return nil, err
}
file.Close()
} else {
return nil, err
}
}
content, err := ioutil.ReadFile(fileName)
if err != nil {
return nil, err
}
if err := yaml.Unmarshal(content, base); err != nil {
return nil, err
}
return base, nil
}
// GetIsNewRepo returns known repo boolean
func (c *AppConfig) GetIsNewRepo() bool {
return c.IsNewRepo
@@ -180,15 +117,10 @@ func (c *AppConfig) GetBuildSource() string {
}
// GetUserConfig returns the user config
func (c *AppConfig) GetUserConfig() *UserConfig {
func (c *AppConfig) GetUserConfig() *viper.Viper {
return c.UserConfig
}
// GetUserConfig returns the user config
func (c *AppConfig) GetUserConfigPath() string {
return c.UserConfigPath
}
// GetAppState returns the app state
func (c *AppConfig) GetAppState() *AppState {
return c.AppState
@@ -198,18 +130,78 @@ func (c *AppConfig) GetUserConfigDir() string {
return c.UserConfigDir
}
func configFilePath(filename string) (string, error) {
folder, err := findOrCreateConfigDir()
func newViper(filename string) (*viper.Viper, error) {
v := viper.New()
v.SetConfigType("yaml")
v.SetConfigName(filename)
return v, nil
}
// LoadConfig gets the user's config
func LoadConfig(filename string, withDefaults bool) (*viper.Viper, string, error) {
v, err := newViper(filename)
if err != nil {
return nil, "", err
}
if withDefaults {
if err = LoadDefaults(v, GetDefaultConfig()); err != nil {
return nil, "", err
}
if err = LoadDefaults(v, GetPlatformDefaultConfig()); err != nil {
return nil, "", err
}
}
configPath, err := LoadAndMergeFile(v, filename+".yml")
if err != nil {
return nil, "", err
}
return v, configPath, nil
}
// LoadDefaults loads in the defaults defined in this file
func LoadDefaults(v *viper.Viper, defaults []byte) error {
return v.MergeConfig(bytes.NewBuffer(defaults))
}
func prepareConfigFile(filename string) (string, error) {
// chucking my name there is not for vanity purposes, the xdg spec (and that
// function) requires a vendor name. May as well line up with github
configDirs := configdir.New("jesseduffield", "lazygit")
folder := configDirs.QueryFolderContainsFile(filename)
if folder == nil {
// create the file as empty
folders := configDirs.QueryFolders(configdir.Global)
if err := folders[0].WriteFile(filename, []byte{}); err != nil {
return "", err
}
folder = configDirs.QueryFolderContainsFile(filename)
}
return filepath.Join(folder.Path, filename), nil
}
// LoadAndMergeFile Loads the config/state file, creating
// the file has an empty one if it does not exist
func LoadAndMergeFile(v *viper.Viper, filename string) (string, error) {
configPath, err := prepareConfigFile(filename)
if err != nil {
return "", err
}
return filepath.Join(folder, filename), nil
v.AddConfigPath(filepath.Dir(configPath))
return configPath, v.MergeInConfig()
}
// ConfigFilename returns the filename of the current config file
func (c *AppConfig) ConfigFilename() string {
return filepath.Join(c.UserConfigDir, "config.yml")
// WriteToUserConfig adds a key/value pair to the user's config and saves it
func (c *AppConfig) WriteToUserConfig(key string, value interface{}) error {
// reloading the user config directly (without defaults) so that we're not
// writing any defaults back to the user's config
v, _, err := LoadConfig("config", false)
if err != nil {
return err
}
v.Set(key, value)
return v.WriteConfig()
}
// SaveAppState marshalls the AppState struct and writes it to the disk
@@ -219,7 +211,7 @@ func (c *AppConfig) SaveAppState() error {
return err
}
filepath, err := configFilePath("state.yml")
filepath, err := prepareConfigFile("state.yml")
if err != nil {
return err
}
@@ -227,47 +219,174 @@ func (c *AppConfig) SaveAppState() error {
return ioutil.WriteFile(filepath, marshalledAppState, 0644)
}
// loadAppState loads recorded AppState from file
func loadAppState() (*AppState, error) {
filepath, err := configFilePath("state.yml")
// LoadAppState loads recorded AppState from file
func (c *AppConfig) LoadAppState() error {
filepath, err := prepareConfigFile("state.yml")
if err != nil {
return nil, err
return err
}
appStateBytes, err := ioutil.ReadFile(filepath)
if err != nil && !os.IsNotExist(err) {
return nil, err
}
if len(appStateBytes) == 0 {
return getDefaultAppState(), nil
}
appState := &AppState{}
err = yaml.Unmarshal(appStateBytes, appState)
if err != nil {
return nil, err
return err
}
if len(appStateBytes) == 0 {
return yaml.Unmarshal(getDefaultAppState(), c.AppState)
}
return yaml.Unmarshal(appStateBytes, c.AppState)
}
return appState, nil
// GetDefaultConfig returns the application default configuration
func GetDefaultConfig() []byte {
return []byte(
`gui:
## stuff relating to the UI
scrollHeight: 2
scrollPastBottom: true
mouseEvents: true
skipUnstageLineWarning: false
theme:
lightTheme: false
activeBorderColor:
- white
- bold
inactiveBorderColor:
- white
optionsTextColor:
- blue
selectedLineBgColor:
- blue
commitLength:
show: true
git:
merging:
manualCommit: false
skipHookPrefix: 'WIP'
autoFetch: true
update:
method: prompt # can be: prompt | background | never
days: 14 # how often a update is checked for
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
splashUpdatesIndex: 0
confirmOnQuit: false
keybinding:
universal:
quit: 'q'
quit-alt1: '<c-c>'
return: '<esc>'
quitWithoutChangingDirectory: 'Q'
togglePanel: '<tab>'
prevItem: '<up>'
nextItem: '<down>'
prevItem-alt: 'k'
nextItem-alt: 'j'
prevBlock: '<left>'
nextBlock: '<right>'
prevBlock-alt: 'h'
nextBlock-alt: 'l'
nextMatch: 'n'
prevMatch: 'N'
startSearch: '/'
optionMenu: 'x'
optionMenu-alt1: '?'
select: '<space>'
goInto: '<enter>'
remove: 'd'
new: 'n'
edit: 'e'
openFile: 'o'
scrollUpMain: '<pgup>'
scrollDownMain: '<pgdown>'
scrollUpMain-alt1: 'K'
scrollDownMain-alt1: 'J'
scrollUpMain-alt2: '<c-u>'
scrollDownMain-alt2: '<c-d>'
executeCustomCommand: ':'
createRebaseOptionsMenu: 'm'
pushFiles: 'P'
pullFiles: 'p'
refresh: 'R'
createPatchOptionsMenu: '<c-p>'
nextTab: ']'
prevTab: '['
nextScreenMode: '+'
prevScreenMode: '_'
status:
checkForUpdate: 'u'
recentRepos: '<enter>'
files:
commitChanges: 'c'
commitChangesWithoutHook: 'w'
amendLastCommit: 'A'
commitChangesWithEditor: 'C'
ignoreFile: 'i'
refreshFiles: 'r'
stashAllChanges: 's'
viewStashOptions: 'S'
toggleStagedAll: 'a'
viewResetOptions: 'D'
fetch: 'f'
branches:
createPullRequest: 'o'
checkoutBranchByName: 'c'
forceCheckoutBranch: 'F'
rebaseBranch: 'r'
mergeIntoCurrentBranch: 'M'
viewGitFlowOptions: 'i'
fastForward: 'f'
pushTag: 'P'
setUpstream: 'u'
fetchRemote: 'f'
commits:
squashDown: 's'
renameCommit: 'r'
renameCommitWithEditor: 'R'
viewResetOptions: 'g'
markCommitAsFixup: 'f'
createFixupCommit: 'F'
squashAboveCommits: 'S'
moveDownCommit: '<c-j>'
moveUpCommit: '<c-k>'
amendToCommit: 'A'
pickCommit: 'p'
revertCommit: 't'
cherryPickCopy: 'c'
cherryPickCopyRange: 'C'
pasteCommits: 'v'
tagCommit: 'T'
toggleDiffCommit: 'i'
checkoutCommit: '<space>'
stash:
popStash: 'g'
commitFiles:
checkoutCommitFile: 'c'
main:
toggleDragSelect: 'v'
toggleDragSelect-alt: 'V'
toggleSelectHunk: 'a'
pickBothHunks: 'b'
undo: 'z'
`)
}
// AppState stores data between runs of the app like when the last update check
// was performed and which other repos have been checked out
type AppState struct {
LastUpdateCheck int64
RecentRepos []string
StartupPopupVersion int
LastUpdateCheck int64
RecentRepos []string
}
func getDefaultAppState() *AppState {
return &AppState{
LastUpdateCheck: 0,
RecentRepos: []string{},
StartupPopupVersion: 0,
}
func getDefaultAppState() []byte {
return []byte(`
lastUpdateCheck: 0
recentRepos: []
`)
}
func LogPath() (string, error) {
return configFilePath("development.log")
}
// // commenting this out until we use it again
// func homeDirectory() string {
// usr, err := user.Current()
// if err != nil {
// log.Fatal(err)
// }
// return usr.HomeDir
// }

View File

@@ -3,9 +3,9 @@
package config
// GetPlatformDefaultConfig gets the defaults for the platform
func GetPlatformDefaultConfig() OSConfig {
return OSConfig{
OpenCommand: "open {{filename}}",
OpenLinkCommand: "open {{link}}",
}
func GetPlatformDefaultConfig() []byte {
return []byte(
`os:
openCommand: 'open {{filename}}'
openLinkCommand: 'open {{link}}'`)
}

View File

@@ -1,9 +1,9 @@
package config
// GetPlatformDefaultConfig gets the defaults for the platform
func GetPlatformDefaultConfig() OSConfig {
return OSConfig{
OpenCommand: `sh -c "xdg-open {{filename}} >/dev/null"`,
OpenLinkCommand: `sh -c "xdg-open {{link}} >/dev/null"`,
}
func GetPlatformDefaultConfig() []byte {
return []byte(
`os:
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'
openLinkCommand: 'sh -c "xdg-open {{link}} >/dev/null"'`)
}

View File

@@ -1,9 +1,9 @@
package config
// GetPlatformDefaultConfig gets the defaults for the platform
func GetPlatformDefaultConfig() OSConfig {
return OSConfig{
OpenCommand: `cmd /c "start "" {{filename}}"`,
OpenLinkCommand: `cmd /c "start "" {{link}}"`,
}
func GetPlatformDefaultConfig() []byte {
return []byte(
`os:
openCommand: 'cmd /c "start "" {{filename}}"'
openLinkCommand: 'cmd /c "start "" {{link}}"'`)
}

View File

@@ -1,20 +0,0 @@
package config
import (
yaml "github.com/jesseduffield/yaml"
)
// NewDummyAppConfig creates a new dummy AppConfig for testing
func NewDummyAppConfig() *AppConfig {
appConfig := &AppConfig{
Name: "lazygit",
Version: "unversioned",
Commit: "",
BuildDate: "",
Debug: false,
BuildSource: "",
UserConfig: GetDefaultConfig(),
}
_ = yaml.Unmarshal([]byte{}, appConfig.AppState)
return appConfig
}

View File

@@ -1,442 +0,0 @@
package config
type UserConfig struct {
Gui GuiConfig `yaml:"gui"`
Git GitConfig `yaml:"git"`
Update UpdateConfig `yaml:"update"`
Reporting string `yaml:"reporting"`
SplashUpdatesIndex int `yaml:"splashUpdatesIndex"`
ConfirmOnQuit bool `yaml:"confirmOnQuit"`
QuitOnTopLevelReturn bool `yaml:"quitOnTopLevelReturn"`
Keybinding KeybindingConfig `yaml:"keybinding"`
// OS determines what defaults are set for opening files and links
OS OSConfig `yaml:"os,omitempty"`
DisableStartupPopups bool `yaml:"disableStartupPopups"`
CustomCommands []CustomCommand `yaml:"customCommands"`
Services map[string]string `yaml:"services"`
}
type GuiConfig struct {
ScrollHeight int `yaml:"scrollHeight"`
ScrollPastBottom bool `yaml:"scrollPastBottom"`
MouseEvents bool `yaml:"mouseEvents"`
SkipUnstageLineWarning bool `yaml:"skipUnstageLineWarning"`
SkipStashWarning bool `yaml:"skipStashWarning"`
SidePanelWidth float64 `yaml:"sidePanelWidth"`
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
MainPanelSplitMode string `yaml:"mainPanelSplitMode"`
Theme ThemeConfig `yaml:"theme"`
CommitLength CommitLengthConfig `yaml:"commitLength"`
}
type ThemeConfig struct {
LightTheme bool `yaml:"lightTheme"`
ActiveBorderColor []string `yaml:"activeBorderColor"`
InactiveBorderColor []string `yaml:"inactiveBorderColor"`
OptionsTextColor []string `yaml:"optionsTextColor"`
SelectedLineBgColor []string `yaml:"selectedLineBgColor"`
SelectedRangeBgColor []string `yaml:"selectedRangeBgColor"`
}
type CommitLengthConfig struct {
Show bool `yaml:"show"`
}
type GitConfig struct {
Paging PagingConfig `yaml:"paging"`
Merging MergingConfig `yaml:"merging"`
Pull PullConfig `yaml:"pull"`
SkipHookPrefix string `yaml:"skipHookPrefix"`
AutoFetch bool `yaml:"autoFetch"`
BranchLogCmd string `yaml:"branchLogCmd"`
OverrideGpg bool `yaml:"overrideGpg"`
DisableForcePushing bool `yaml:"disableForcePushing"`
CommitPrefixes map[string]CommitPrefixConfig `yaml:"commitPrefixes"`
}
type PagingConfig struct {
ColorArg string `yaml:"colorArg"`
Pager string `yaml:"pager"`
UseConfig bool `yaml:"useConfig"`
}
type MergingConfig struct {
ManualCommit bool `yaml:"manualCommit"`
Args string `yaml:"args"`
}
type PullConfig struct {
Mode string `yaml:"mode"`
}
type CommitPrefixConfig struct {
Pattern string `yaml:"pattern"`
Replace string `yaml:"replace"`
}
type UpdateConfig struct {
Method string `yaml:"method"`
Days int64 `yaml:"days"`
}
type KeybindingConfig struct {
Universal KeybindingUniversalConfig `yaml:"universal"`
Status KeybindingStatusConfig `yaml:"status"`
Files KeybindingFilesConfig `yaml:"files"`
Branches KeybindingBranchesConfig `yaml:"branches"`
Commits KeybindingCommitsConfig `yaml:"commits"`
Stash KeybindingStashConfig `yaml:"stash"`
CommitFiles KeybindingCommitFilesConfig `yaml:"commitFiles"`
Main KeybindingMainConfig `yaml:"main"`
Submodules KeybindingSubmodulesConfig `yaml:"submodules"`
}
type KeybindingUniversalConfig struct {
Quit string `yaml:"quit"`
QuitAlt1 string `yaml:"quit-alt1"`
Return string `yaml:"return"`
QuitWithoutChangingDirectory string `yaml:"quitWithoutChangingDirectory"`
TogglePanel string `yaml:"togglePanel"`
PrevItem string `yaml:"prevItem"`
NextItem string `yaml:"nextItem"`
PrevItemAlt string `yaml:"prevItem-alt"`
NextItemAlt string `yaml:"nextItem-alt"`
PrevPage string `yaml:"prevPage"`
NextPage string `yaml:"nextPage"`
GotoTop string `yaml:"gotoTop"`
GotoBottom string `yaml:"gotoBottom"`
PrevBlock string `yaml:"prevBlock"`
NextBlock string `yaml:"nextBlock"`
PrevBlockAlt string `yaml:"prevBlock-alt"`
NextBlockAlt string `yaml:"nextBlock-alt"`
NextMatch string `yaml:"nextMatch"`
PrevMatch string `yaml:"prevMatch"`
StartSearch string `yaml:"startSearch"`
OptionMenu string `yaml:"optionMenu"`
OptionMenuAlt1 string `yaml:"optionMenu-alt1"`
Select string `yaml:"select"`
GoInto string `yaml:"goInto"`
Confirm string `yaml:"confirm"`
ConfirmAlt1 string `yaml:"confirm-alt1"`
Remove string `yaml:"remove"`
New string `yaml:"new"`
Edit string `yaml:"edit"`
OpenFile string `yaml:"openFile"`
ScrollUpMain string `yaml:"scrollUpMain"`
ScrollDownMain string `yaml:"scrollDownMain"`
ScrollUpMainAlt1 string `yaml:"scrollUpMain-alt1"`
ScrollDownMainAlt1 string `yaml:"scrollDownMain-alt1"`
ScrollUpMainAlt2 string `yaml:"scrollUpMain-alt2"`
ScrollDownMainAlt2 string `yaml:"scrollDownMain-alt2"`
ExecuteCustomCommand string `yaml:"executeCustomCommand"`
CreateRebaseOptionsMenu string `yaml:"createRebaseOptionsMenu"`
PushFiles string `yaml:"pushFiles"`
PullFiles string `yaml:"pullFiles"`
Refresh string `yaml:"refresh"`
CreatePatchOptionsMenu string `yaml:"createPatchOptionsMenu"`
NextTab string `yaml:"nextTab"`
PrevTab string `yaml:"prevTab"`
NextScreenMode string `yaml:"nextScreenMode"`
PrevScreenMode string `yaml:"prevScreenMode"`
Undo string `yaml:"undo"`
Redo string `yaml:"redo"`
FilteringMenu string `yaml:"filteringMenu"`
DiffingMenu string `yaml:"diffingMenu"`
DiffingMenuAlt string `yaml:"diffingMenu-alt"`
CopyToClipboard string `yaml:"copyToClipboard"`
SubmitEditorText string `yaml:"submitEditorText"`
AppendNewline string `yaml:"appendNewline"`
}
type KeybindingStatusConfig struct {
CheckForUpdate string `yaml:"checkForUpdate"`
RecentRepos string `yaml:"recentRepos"`
}
type KeybindingFilesConfig struct {
CommitChanges string `yaml:"commitChanges"`
CommitChangesWithoutHook string `yaml:"commitChangesWithoutHook"`
AmendLastCommit string `yaml:"amendLastCommit"`
CommitChangesWithEditor string `yaml:"commitChangesWithEditor"`
IgnoreFile string `yaml:"ignoreFile"`
RefreshFiles string `yaml:"refreshFiles"`
StashAllChanges string `yaml:"stashAllChanges"`
ViewStashOptions string `yaml:"viewStashOptions"`
ToggleStagedAll string `yaml:"toggleStagedAll"`
ViewResetOptions string `yaml:"viewResetOptions"`
Fetch string `yaml:"fetch"`
}
type KeybindingBranchesConfig struct {
CreatePullRequest string `yaml:"createPullRequest"`
CheckoutBranchByName string `yaml:"checkoutBranchByName"`
ForceCheckoutBranch string `yaml:"forceCheckoutBranch"`
RebaseBranch string `yaml:"rebaseBranch"`
RenameBranch string `yaml:"renameBranch"`
MergeIntoCurrentBranch string `yaml:"mergeIntoCurrentBranch"`
ViewGitFlowOptions string `yaml:"viewGitFlowOptions"`
FastForward string `yaml:"fastForward"`
PushTag string `yaml:"pushTag"`
SetUpstream string `yaml:"setUpstream"`
FetchRemote string `yaml:"fetchRemote"`
}
type KeybindingCommitsConfig struct {
SquashDown string `yaml:"squashDown"`
RenameCommit string `yaml:"renameCommit"`
RenameCommitWithEditor string `yaml:"renameCommitWithEditor"`
ViewResetOptions string `yaml:"viewResetOptions"`
MarkCommitAsFixup string `yaml:"markCommitAsFixup"`
CreateFixupCommit string `yaml:"createFixupCommit"`
SquashAboveCommits string `yaml:"squashAboveCommits"`
MoveDownCommit string `yaml:"moveDownCommit"`
MoveUpCommit string `yaml:"moveUpCommit"`
AmendToCommit string `yaml:"amendToCommit"`
PickCommit string `yaml:"pickCommit"`
RevertCommit string `yaml:"revertCommit"`
CherryPickCopy string `yaml:"cherryPickCopy"`
CherryPickCopyRange string `yaml:"cherryPickCopyRange"`
PasteCommits string `yaml:"pasteCommits"`
TagCommit string `yaml:"tagCommit"`
CheckoutCommit string `yaml:"checkoutCommit"`
ResetCherryPick string `yaml:"resetCherryPick"`
CopyCommitMessageToClipboard string `yaml:"copyCommitMessageToClipboard"`
}
type KeybindingStashConfig struct {
PopStash string `yaml:"popStash"`
}
type KeybindingCommitFilesConfig struct {
CheckoutCommitFile string `yaml:"checkoutCommitFile"`
}
type KeybindingMainConfig struct {
ToggleDragSelect string `yaml:"toggleDragSelect"`
ToggleDragSelectAlt string `yaml:"toggleDragSelect-alt"`
ToggleSelectHunk string `yaml:"toggleSelectHunk"`
PickBothHunks string `yaml:"pickBothHunks"`
}
type KeybindingSubmodulesConfig struct {
Init string `yaml:"init"`
Update string `yaml:"update"`
BulkMenu string `yaml:"bulkMenu"`
}
// OSConfig contains config on the level of the os
type OSConfig struct {
// OpenCommand is the command for opening a file
OpenCommand string `yaml:"openCommand,omitempty"`
// OpenCommand is the command for opening a link
OpenLinkCommand string `yaml:"openLinkCommand,omitempty"`
}
type CustomCommand struct {
Key string `yaml:"key"`
Context string `yaml:"context"`
Command string `yaml:"command"`
Subprocess bool `yaml:"subprocess"`
Prompts []CustomCommandPrompt `yaml:"prompts"`
LoadingText string `yaml:"loadingText"`
Description string `yaml:"description"`
}
type CustomCommandPrompt struct {
Type string `yaml:"type"` // one of 'input' and 'menu'
Title string `yaml:"title"`
// this only apply to prompts
InitialValue string `yaml:"initialValue"`
// this only applies to menus
Options []CustomCommandMenuOption
}
type CustomCommandMenuOption struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
Value string `yaml:"value"`
}
func GetDefaultConfig() *UserConfig {
return &UserConfig{
Gui: GuiConfig{
ScrollHeight: 2,
ScrollPastBottom: true,
MouseEvents: true,
SkipUnstageLineWarning: false,
SkipStashWarning: true,
SidePanelWidth: 0.3333,
ExpandFocusedSidePanel: false,
MainPanelSplitMode: "flexible",
Theme: ThemeConfig{
LightTheme: false,
ActiveBorderColor: []string{"green", "bold"},
InactiveBorderColor: []string{"white"},
OptionsTextColor: []string{"blue"},
SelectedLineBgColor: []string{"default"},
SelectedRangeBgColor: []string{"blue"},
},
CommitLength: CommitLengthConfig{Show: true},
},
Git: GitConfig{
Paging: PagingConfig{
ColorArg: "always",
Pager: "",
UseConfig: false},
Merging: MergingConfig{
ManualCommit: false,
Args: "",
},
Pull: PullConfig{
Mode: "merge",
},
SkipHookPrefix: "WIP",
AutoFetch: true,
BranchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --",
OverrideGpg: false,
DisableForcePushing: false,
CommitPrefixes: map[string]CommitPrefixConfig(nil),
},
Update: UpdateConfig{
Method: "prompt",
Days: 14,
},
Reporting: "undetermined",
SplashUpdatesIndex: 0,
ConfirmOnQuit: false,
QuitOnTopLevelReturn: true,
Keybinding: KeybindingConfig{
Universal: KeybindingUniversalConfig{
Quit: "q",
QuitAlt1: "<c-c>",
Return: "<esc>",
QuitWithoutChangingDirectory: "Q",
TogglePanel: "<tab>",
PrevItem: "<up>",
NextItem: "<down>",
PrevItemAlt: "k",
NextItemAlt: "j",
PrevPage: ",",
NextPage: ".",
GotoTop: "<",
GotoBottom: ">",
PrevBlock: "<left>",
NextBlock: "<right>",
PrevBlockAlt: "h",
NextBlockAlt: "l",
NextMatch: "n",
PrevMatch: "N",
StartSearch: "/",
OptionMenu: "x",
OptionMenuAlt1: "?",
Select: "<space>",
GoInto: "<enter>",
Confirm: "<enter>",
ConfirmAlt1: "y",
Remove: "d",
New: "n",
Edit: "e",
OpenFile: "o",
ScrollUpMain: "<pgup>",
ScrollDownMain: "<pgdown>",
ScrollUpMainAlt1: "K",
ScrollDownMainAlt1: "J",
ScrollUpMainAlt2: "<c-u>",
ScrollDownMainAlt2: "<c-d>",
ExecuteCustomCommand: ":",
CreateRebaseOptionsMenu: "m",
PushFiles: "P",
PullFiles: "p",
Refresh: "R",
CreatePatchOptionsMenu: "<c-p>",
NextTab: "]",
PrevTab: "[",
NextScreenMode: "+",
PrevScreenMode: "_",
Undo: "z",
Redo: "<c-z>",
FilteringMenu: "<c-s>",
DiffingMenu: "W",
DiffingMenuAlt: "<c-e>",
CopyToClipboard: "<c-o>",
SubmitEditorText: "<enter>",
AppendNewline: "<tab>",
},
Status: KeybindingStatusConfig{
CheckForUpdate: "u",
RecentRepos: "<enter>",
},
Files: KeybindingFilesConfig{
CommitChanges: "c",
CommitChangesWithoutHook: "w",
AmendLastCommit: "A",
CommitChangesWithEditor: "C",
IgnoreFile: "i",
RefreshFiles: "r",
StashAllChanges: "s",
ViewStashOptions: "S",
ToggleStagedAll: "a",
ViewResetOptions: "D",
Fetch: "f",
},
Branches: KeybindingBranchesConfig{
CreatePullRequest: "o",
CheckoutBranchByName: "c",
ForceCheckoutBranch: "F",
RebaseBranch: "r",
RenameBranch: "R",
MergeIntoCurrentBranch: "M",
ViewGitFlowOptions: "i",
FastForward: "f",
PushTag: "P",
SetUpstream: "u",
FetchRemote: "f",
},
Commits: KeybindingCommitsConfig{
SquashDown: "s",
RenameCommit: "r",
RenameCommitWithEditor: "R",
ViewResetOptions: "g",
MarkCommitAsFixup: "f",
CreateFixupCommit: "F",
SquashAboveCommits: "S",
MoveDownCommit: "<c-j>",
MoveUpCommit: "<c-k>",
AmendToCommit: "A",
PickCommit: "p",
RevertCommit: "t",
CherryPickCopy: "c",
CherryPickCopyRange: "C",
PasteCommits: "v",
TagCommit: "T",
CheckoutCommit: "<space>",
ResetCherryPick: "<c-R>",
CopyCommitMessageToClipboard: "<c-y>",
},
Stash: KeybindingStashConfig{
PopStash: "g",
},
CommitFiles: KeybindingCommitFilesConfig{
CheckoutCommitFile: "c",
},
Main: KeybindingMainConfig{
ToggleDragSelect: "v",
ToggleDragSelectAlt: "V",
ToggleSelectHunk: "a",
PickBothHunks: "b",
},
Submodules: KeybindingSubmodulesConfig{
Init: "i",
Update: "u",
BulkMenu: "b",
},
},
OS: GetPlatformDefaultConfig(),
DisableStartupPopups: false,
CustomCommands: []CustomCommand(nil),
Services: map[string]string(nil),
}
}

24
pkg/env/env.go vendored
View File

@@ -1,24 +0,0 @@
package env
import "os"
func GetGitDirEnv() string {
return os.Getenv("GIT_DIR")
}
func GetGitWorkTreeEnv() string {
return os.Getenv("GIT_WORK_TREE")
}
func SetGitDirEnv(value string) {
os.Setenv("GIT_DIR", value)
}
func SetGitWorkTreeEnv(value string) {
os.Setenv("GIT_WORK_TREE", value)
}
func UnsetGitDirEnvs() {
_ = os.Unsetenv("GIT_DIR")
_ = os.Unsetenv("GIT_WORK_TREE")
}

View File

@@ -50,31 +50,34 @@ func (m *statusManager) getStatusString() string {
// WithWaitingStatus wraps a function and shows a waiting status while the function is still executing
func (gui *Gui) WithWaitingStatus(name string, f func() error) error {
go utils.Safe(func() {
go func() {
gui.statusManager.addWaitingStatus(name)
defer func() {
gui.statusManager.removeStatus(name)
}()
go utils.Safe(func() {
go func() {
ticker := time.NewTicker(time.Millisecond * 50)
defer ticker.Stop()
for range ticker.C {
appStatus := gui.statusManager.getStatusString()
gui.Log.Warn(appStatus)
if appStatus == "" {
return
}
gui.renderString("appStatus", appStatus)
if err := gui.renderString(gui.g, "appStatus", appStatus); err != nil {
gui.Log.Warn(err)
}
}
})
}()
if err := f(); err != nil {
gui.g.Update(func(g *gocui.Gui) error {
return gui.surfaceError(err)
return gui.createErrorPanel(gui.g, err.Error())
})
}
})
}()
return nil
}

View File

@@ -1,291 +0,0 @@
package gui
import (
"github.com/jesseduffield/lazygit/pkg/gui/boxlayout"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) mainSectionChildren() []*boxlayout.Box {
currentWindow := gui.currentWindow()
// if we're not in split mode we can just show the one main panel. Likewise if
// the main panel is focused and we're in full-screen mode
if !gui.isMainPanelSplit() || (gui.State.ScreenMode == SCREEN_FULL && currentWindow == "main") {
return []*boxlayout.Box{
{
Window: "main",
Weight: 1,
},
}
}
main := "main"
secondary := "secondary"
if gui.secondaryViewFocused() {
// when you think you've focused the secondary view, we've actually just swapped them around in the layout
main, secondary = secondary, main
}
return []*boxlayout.Box{
{
Window: main,
Weight: 1,
},
{
Window: secondary,
Weight: 1,
},
}
}
func (gui *Gui) getMidSectionWeights() (int, int) {
currentWindow := gui.currentWindow()
// we originally specified this as a ratio i.e. .20 would correspond to a weight of 1 against 4
sidePanelWidthRatio := gui.Config.GetUserConfig().Gui.SidePanelWidth
// we could make this better by creating ratios like 2:3 rather than always 1:something
mainSectionWeight := int(1/sidePanelWidthRatio) - 1
sideSectionWeight := 1
if gui.splitMainPanelSideBySide() {
mainSectionWeight = 5 // need to shrink side panel to make way for main panels if side-by-side
}
if currentWindow == "main" {
if gui.State.ScreenMode == SCREEN_HALF || gui.State.ScreenMode == SCREEN_FULL {
sideSectionWeight = 0
}
} else {
if gui.State.ScreenMode == SCREEN_HALF {
mainSectionWeight = 1
} else if gui.State.ScreenMode == SCREEN_FULL {
mainSectionWeight = 0
}
}
return sideSectionWeight, mainSectionWeight
}
func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*boxlayout.Box {
if gui.State.Searching.isSearching {
return []*boxlayout.Box{
{
Window: "searchPrefix",
Size: len(SEARCH_PREFIX),
},
{
Window: "search",
Weight: 1,
},
}
}
result := []*boxlayout.Box{}
if len(appStatus) > 0 {
result = append(result,
&boxlayout.Box{
Window: "appStatus",
Size: len(appStatus) + len(INFO_SECTION_PADDING),
},
)
}
result = append(result,
[]*boxlayout.Box{
{
Window: "options",
Weight: 1,
},
{
Window: "information",
// unlike appStatus, informationStr has various colors so we need to decolorise before taking the length
Size: len(INFO_SECTION_PADDING) + len(utils.Decolorise(informationStr)),
},
}...,
)
return result
}
func (gui *Gui) splitMainPanelSideBySide() bool {
if !gui.isMainPanelSplit() {
return false
}
mainPanelSplitMode := gui.Config.GetUserConfig().Gui.MainPanelSplitMode
width, height := gui.g.Size()
switch mainPanelSplitMode {
case "vertical":
return false
case "horizontal":
return true
default:
if width < 200 && height > 30 { // 2 80 character width panels + 40 width for side panel
return false
} else {
return true
}
}
}
func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions {
width, height := gui.g.Size()
sideSectionWeight, mainSectionWeight := gui.getMidSectionWeights()
sidePanelsDirection := boxlayout.COLUMN
portraitMode := width <= 84 && height > 45
if portraitMode {
sidePanelsDirection = boxlayout.ROW
}
mainPanelsDirection := boxlayout.ROW
if gui.splitMainPanelSideBySide() {
mainPanelsDirection = boxlayout.COLUMN
}
root := &boxlayout.Box{
Direction: boxlayout.ROW,
Children: []*boxlayout.Box{
{
Direction: sidePanelsDirection,
Weight: 1,
Children: []*boxlayout.Box{
{
Direction: boxlayout.ROW,
Weight: sideSectionWeight,
ConditionalChildren: gui.sidePanelChildren,
},
{
Direction: mainPanelsDirection,
Weight: mainSectionWeight,
Children: gui.mainSectionChildren(),
},
},
},
{
Direction: boxlayout.COLUMN,
Size: 1,
Children: gui.infoSectionChildren(informationStr, appStatus),
},
},
}
return boxlayout.ArrangeWindows(root, 0, 0, width, height)
}
// The stash window by default only contains one line so that it's not hogging
// too much space, but if you access it it should take up some space. This is
// the default behaviour when accordian mode is NOT in effect. If it is in effect
// then when it's accessed it will have weight 2, not 1.
func (gui *Gui) getDefaultStashWindowBox() *boxlayout.Box {
box := &boxlayout.Box{Window: "stash"}
stashWindowAccessed := false
for _, context := range gui.State.ContextStack {
if context.GetWindowName() == "stash" {
stashWindowAccessed = true
}
}
// if the stash window is anywhere in our stack we should enlargen it
if stashWindowAccessed {
box.Weight = 1
} else {
box.Size = 3
}
return box
}
func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
currentWindow := gui.currentSideWindowName()
if gui.State.ScreenMode == SCREEN_FULL || gui.State.ScreenMode == SCREEN_HALF {
fullHeightBox := func(window string) *boxlayout.Box {
if window == currentWindow {
return &boxlayout.Box{
Window: window,
Weight: 1,
}
} else {
return &boxlayout.Box{
Window: window,
Size: 0,
}
}
}
return []*boxlayout.Box{
fullHeightBox("status"),
fullHeightBox("files"),
fullHeightBox("branches"),
fullHeightBox("commits"),
fullHeightBox("stash"),
}
} else if height >= 28 {
accordianMode := gui.Config.GetUserConfig().Gui.ExpandFocusedSidePanel
accordianBox := func(defaultBox *boxlayout.Box) *boxlayout.Box {
if accordianMode && defaultBox.Window == currentWindow {
return &boxlayout.Box{
Window: defaultBox.Window,
Weight: 2,
}
}
return defaultBox
}
return []*boxlayout.Box{
{
Window: "status",
Size: 3,
},
accordianBox(&boxlayout.Box{Window: "files", Weight: 1}),
accordianBox(&boxlayout.Box{Window: "branches", Weight: 1}),
accordianBox(&boxlayout.Box{Window: "commits", Weight: 1}),
accordianBox(gui.getDefaultStashWindowBox()),
}
} else {
squashedHeight := 1
if height >= 21 {
squashedHeight = 3
}
squashedSidePanelBox := func(window string) *boxlayout.Box {
if window == currentWindow {
return &boxlayout.Box{
Window: window,
Weight: 1,
}
} else {
return &boxlayout.Box{
Window: window,
Size: squashedHeight,
}
}
}
return []*boxlayout.Box{
squashedSidePanelBox("status"),
squashedSidePanelBox("files"),
squashedSidePanelBox("branches"),
squashedSidePanelBox("commits"),
squashedSidePanelBox("stash"),
}
}
}
func (gui *Gui) currentSideWindowName() string {
// there is always one and only one cyclable context in the context stack. We'll look from top to bottom
for idx := range gui.State.ContextStack {
reversedIdx := len(gui.State.ContextStack) - 1 - idx
context := gui.State.ContextStack[reversedIdx]
if context.GetKind() == SIDE_CONTEXT {
return context.GetWindowName()
}
}
return "files" // default
}

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