mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-28 10:16:22 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fb478a30e | ||
|
|
5d12a6bf99 | ||
|
|
1d40d03bb2 | ||
|
|
9a9e3d506d | ||
|
|
06ca71e955 | ||
|
|
308a3b51b3 | ||
|
|
ccd80a0e4b | ||
|
|
37be9dbea1 | ||
|
|
f6ec7babf5 | ||
|
|
802cfb1a04 | ||
|
|
2fc1498517 | ||
|
|
7a464ae5b7 | ||
|
|
927ee63106 | ||
|
|
9989c96321 | ||
|
|
f91892b8f1 | ||
|
|
72bce201df | ||
|
|
5df0fe0765 | ||
|
|
c47c539e12 | ||
|
|
c96496c3a7 | ||
|
|
7561703e8d | ||
|
|
b04b457246 | ||
|
|
6457800748 | ||
|
|
7d9461877a | ||
|
|
e122f421e6 | ||
|
|
6171690b00 | ||
|
|
253504a094 | ||
|
|
f704707d29 | ||
|
|
01d82749b1 | ||
|
|
3eb124c732 | ||
|
|
ef544e6ce9 | ||
|
|
629494144f | ||
|
|
b6a5e9d615 | ||
|
|
5011cac7ea | ||
|
|
5df0475612 | ||
|
|
f6e316dfe5 | ||
|
|
91e8765d9c | ||
|
|
80a8e9b04d | ||
|
|
2008c39516 | ||
|
|
6388af70ac | ||
|
|
5ee559b896 | ||
|
|
ca7252ef8e | ||
|
|
a496858c62 | ||
|
|
40bc3aa5a9 | ||
|
|
e4888e924e | ||
|
|
71fdc5c038 | ||
|
|
a05f22efa2 | ||
|
|
c0cd9dd835 | ||
|
|
305f211615 | ||
|
|
d672b7342f | ||
|
|
e7c27b6f4a | ||
|
|
345c90ac05 | ||
|
|
7564e506b5 | ||
|
|
1e50764b4d | ||
|
|
9619d3447f | ||
|
|
4171b7613c | ||
|
|
92f03a7872 | ||
|
|
2dc8396deb | ||
|
|
7b615e3186 | ||
|
|
a2108362de | ||
|
|
87e9d9bdc2 | ||
|
|
b6454755ca | ||
|
|
3621084096 | ||
|
|
8c25aaa687 | ||
|
|
d02e52989e | ||
|
|
913a2fd065 | ||
|
|
db736896bc | ||
|
|
154b6b09cb | ||
|
|
292b780bd8 | ||
|
|
c421f396af | ||
|
|
a1ae2aa277 | ||
|
|
e19b4fe369 | ||
|
|
eb7531b206 | ||
|
|
428ce2d0f2 | ||
|
|
f1fbf1e9f5 | ||
|
|
268d4080b3 | ||
|
|
2c72990838 | ||
|
|
046edd8120 | ||
|
|
c4552aad28 | ||
|
|
5c57c973d6 | ||
|
|
c5f7ad5adb | ||
|
|
663c036ca5 | ||
|
|
c8e9d1b4fc | ||
|
|
ab0117c416 | ||
|
|
652c97d239 | ||
|
|
bd67bba751 | ||
|
|
5193353020 | ||
|
|
a5719c530a | ||
|
|
add3e8783e | ||
|
|
5eff56b557 | ||
|
|
60c87b3e70 | ||
|
|
66d0fd2133 | ||
|
|
f44ae68e99 | ||
|
|
57f7051590 | ||
|
|
0543d43f10 | ||
|
|
ab8f2b7cc4 | ||
|
|
16dbb6f76e | ||
|
|
51383f24bf | ||
|
|
151486dcfb | ||
|
|
c1d2aa61f3 | ||
|
|
63072af5bc | ||
|
|
44d08edfb0 | ||
|
|
f08fdb2873 | ||
|
|
df4eb70ba2 | ||
|
|
6ca42ff720 | ||
|
|
a533f8e1a5 | ||
|
|
cf8ded0b79 | ||
|
|
73548fa15f | ||
|
|
a0e7604f61 | ||
|
|
aedeba4fe3 | ||
|
|
7033a4bd58 | ||
|
|
c3d7de1c18 | ||
|
|
711bd5a670 | ||
|
|
6b68f4f25d | ||
|
|
89ee0a1dee | ||
|
|
2dc6f5f079 | ||
|
|
bdea3b7dcf | ||
|
|
51f05ce08b | ||
|
|
487ad196a7 | ||
|
|
44140adb92 | ||
|
|
508af269fb | ||
|
|
0af0e66586 | ||
|
|
821a59f21d | ||
|
|
5ea3dc7579 | ||
|
|
2eeff1257b | ||
|
|
c878f34ff1 | ||
|
|
f8db3592e3 | ||
|
|
d073932cec | ||
|
|
a2f7fcd730 | ||
|
|
f96674b24b | ||
|
|
a553f7fb77 | ||
|
|
6c415d1341 | ||
|
|
617e8a05ee | ||
|
|
b21ac990ea | ||
|
|
0740409f43 | ||
|
|
37700908cc | ||
|
|
488c43aaa2 | ||
|
|
bb4fe2653b | ||
|
|
a2ee52142c | ||
|
|
66d735acb5 | ||
|
|
d51b065f2a | ||
|
|
a3a14e9ff4 | ||
|
|
e58376f9f7 | ||
|
|
e8e4fa5957 | ||
|
|
b5d8849c06 | ||
|
|
5d1a9639b6 | ||
|
|
ea136e4e77 | ||
|
|
dcd3b7c058 | ||
|
|
fd8cb6e6d7 | ||
|
|
906ec30cac | ||
|
|
46c146a8c1 | ||
|
|
a8ec044f0e | ||
|
|
ac609bd37c | ||
|
|
67cc65930a | ||
|
|
4f66093335 | ||
|
|
d626bcac00 | ||
|
|
123d624141 | ||
|
|
e798aa4b15 | ||
|
|
04e474aa66 | ||
|
|
0662733ad9 | ||
|
|
3c78ba7ed3 | ||
|
|
550c0fd4dc | ||
|
|
0bc0e4ac88 | ||
|
|
117c0bd4f7 | ||
|
|
79848087bc | ||
|
|
a3b820fb5f | ||
|
|
de5133ff90 | ||
|
|
1183de151a | ||
|
|
bfc9881213 | ||
|
|
3db40a79fe | ||
|
|
62393cf28a | ||
|
|
ec82f8099c | ||
|
|
b81bac3d65 | ||
|
|
58ddbae4d1 | ||
|
|
3802b563b0 | ||
|
|
d1134daa53 | ||
|
|
63cb304a82 | ||
|
|
6e579dc6e4 | ||
|
|
d5ec0fdcd1 | ||
|
|
0a63f701e5 | ||
|
|
bccf203a18 | ||
|
|
b590397dce | ||
|
|
755cc9f8d8 | ||
|
|
0e6598adbd | ||
|
|
f2645da16a | ||
|
|
f8f596d097 | ||
|
|
028cb2be2f | ||
|
|
fb69bfd20d | ||
|
|
f4874bbb74 | ||
|
|
eec20b845d | ||
|
|
3a0a9ec33b | ||
|
|
9b57b73f41 | ||
|
|
4fca89bc52 | ||
|
|
fc76b44b45 | ||
|
|
9a087d04eb | ||
|
|
c005b0d92b | ||
|
|
713fae3e32 | ||
|
|
148bf2c070 | ||
|
|
edfb0a26b2 | ||
|
|
f70435a20f | ||
|
|
b92ff3ee3f | ||
|
|
f1ced5539a | ||
|
|
77e9ee64a4 | ||
|
|
9daa47fb2d | ||
|
|
d18c8c8dc3 | ||
|
|
1573a449f8 | ||
|
|
7b19c5ad95 | ||
|
|
b363b75534 | ||
|
|
fc066d2f2e | ||
|
|
53ea7df655 | ||
|
|
533817bda3 | ||
|
|
35f1ccdb1b | ||
|
|
3dc3174d85 | ||
|
|
ae2496cf80 | ||
|
|
2ac33bb83d | ||
|
|
2b4048ebff | ||
|
|
31bcd632c7 | ||
|
|
aa9ef12d43 | ||
|
|
b80fafef02 | ||
|
|
130480555f | ||
|
|
92cc6e883d | ||
|
|
107503c903 | ||
|
|
7ae106d4df | ||
|
|
16dcc8f4db | ||
|
|
eb10ddfccc | ||
|
|
22a6771e51 | ||
|
|
3f96537380 | ||
|
|
a9f04d3925 | ||
|
|
83834a2c2e | ||
|
|
0c3132c6f0 | ||
|
|
b28569a593 | ||
|
|
1aa45b0142 | ||
|
|
39c8577074 | ||
|
|
23285eab40 | ||
|
|
0c2d90a444 | ||
|
|
d65c018875 | ||
|
|
0c135515a5 | ||
|
|
2b9df0ea06 | ||
|
|
b7b30191f1 | ||
|
|
7d1b76a349 | ||
|
|
40f10c3388 | ||
|
|
01e4467d76 | ||
|
|
b4e6850f98 | ||
|
|
c57a0077d0 | ||
|
|
46e500dc28 | ||
|
|
d7865b3882 | ||
|
|
0aad68acf0 | ||
|
|
4969e9ce0a | ||
|
|
17770b9f9b | ||
|
|
3dd88d6138 | ||
|
|
ce7cbe58a0 | ||
|
|
7588d5290b | ||
|
|
9fdf92b226 | ||
|
|
93bf691fd6 | ||
|
|
82022615dd | ||
|
|
fb395bca6e | ||
|
|
f91adf026b | ||
|
|
6d91661d5e | ||
|
|
90983aae65 | ||
|
|
f71b23b890 | ||
|
|
05a23f0e1e | ||
|
|
fd38ad8096 | ||
|
|
d502c43ae8 | ||
|
|
0df02dacc2 | ||
|
|
3258c24fb3 | ||
|
|
e7c657fba0 | ||
|
|
60468d2e17 | ||
|
|
cb78cf7de4 | ||
|
|
94b52af661 | ||
|
|
472288c81b | ||
|
|
258eedb38c | ||
|
|
bc044c64b2 | ||
|
|
e478c254d4 | ||
|
|
44f7fc6f7c | ||
|
|
a13e919d3d | ||
|
|
f92fcfbb47 | ||
|
|
6ccf58c224 | ||
|
|
9190e9beac |
@@ -0,0 +1,9 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
allowed_updates:
|
||||
- match:
|
||||
update_type: "security"
|
||||
@@ -29,26 +29,3 @@ jobs:
|
||||
with:
|
||||
token: ${{secrets.GITHUB_API_TOKEN}}
|
||||
formula: lazygit
|
||||
ppa:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout PPA repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: dawidd6/lazygit-debian
|
||||
token: ${{secrets.GITHUB_API_TOKEN}}
|
||||
fetch-depth: 0
|
||||
- name: Setup git
|
||||
uses: dawidd6/action-git-user-config@v1
|
||||
- name: Update PPA repo
|
||||
run: |
|
||||
version="$(echo "$GITHUB_REF" | sed 's@refs/tags/v@@')"
|
||||
sudo apt update
|
||||
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 -m "d/changelog: dch $version"
|
||||
gbp tag
|
||||
git push --tags origin master
|
||||
|
||||
+37
-12
@@ -8,7 +8,14 @@ on:
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- windows-latest
|
||||
name: ci - ${{matrix.os}}
|
||||
runs-on: ${{matrix.os}}
|
||||
env:
|
||||
GOFLAGS: -mod=vendor
|
||||
steps:
|
||||
@@ -22,19 +29,37 @@ jobs:
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cache/go-build
|
||||
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}
|
||||
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-test
|
||||
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
|
||||
bash ./test.sh
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GOFLAGS: -mod=vendor
|
||||
GOARCH: amd64
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
args: --skip-publish --snapshot
|
||||
go-version: 1.16.x
|
||||
- name: Cache build
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cache/go-build
|
||||
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-build
|
||||
restore-keys: |
|
||||
${{runner.os}}-go-
|
||||
- name: Build linux binary
|
||||
run: |
|
||||
GOOS=linux go build
|
||||
- name: Build windows binary
|
||||
run: |
|
||||
GOOS=windows go build
|
||||
- name: Build darwin binary
|
||||
run: |
|
||||
GOOS=darwin go build
|
||||
|
||||
@@ -12,3 +12,9 @@ jobs:
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
with:
|
||||
version: latest
|
||||
- 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
|
||||
|
||||
+3
-1
@@ -27,8 +27,10 @@ lazygit
|
||||
|
||||
test/git_server/data
|
||||
test/integration/*/actual/
|
||||
test/integration/*/actual_remote/
|
||||
test/integration/*/used_config/
|
||||
# these sample hooks waste too space space
|
||||
# these sample hooks waste too much space
|
||||
test/integration/*/expected/.git_keep/hooks/
|
||||
test/integration/*/expected_remote/hooks/
|
||||
!.git_keep/
|
||||
lazygit.exe
|
||||
|
||||
+20
-3
@@ -1,14 +1,13 @@
|
||||
# Contributing
|
||||
|
||||
|
||||
♥ We love pull requests from everyone !
|
||||
|
||||
|
||||
When contributing to this repository, please first discuss the change you wish
|
||||
to make via issue, email, or any other method with the owners of this repository
|
||||
before making a change.
|
||||
before making a change.
|
||||
|
||||
## So all code changes happen through Pull Requests
|
||||
|
||||
Pull requests are the best way to propose changes to the codebase. We actively
|
||||
welcome your pull requests:
|
||||
|
||||
@@ -21,15 +20,33 @@ welcome your pull requests:
|
||||
7. Issue that pull request!
|
||||
|
||||
## Code of conduct
|
||||
|
||||
Please note by participating in this project, you agree to abide by the [code of conduct].
|
||||
|
||||
[code of conduct]: https://github.com/jesseduffield/lazygit/blob/master/CODE-OF-CONDUCT.md
|
||||
|
||||
## Any contributions you make will be under the MIT Software License
|
||||
|
||||
In short, when you submit code changes, your submissions are understood to be
|
||||
under the same [MIT License](http://choosealicense.com/licenses/mit/) that
|
||||
covers the project. Feel free to contact the maintainers if that's a concern.
|
||||
|
||||
## Report bugs using Github's [issues](https://github.com/jesseduffield/lazygit/issues)
|
||||
|
||||
We use GitHub issues to track public bugs. Report a bug by [opening a new
|
||||
issue](https://github.com/jesseduffield/lazygit/issues/new); it's that easy!
|
||||
|
||||
## Updating Gocui
|
||||
|
||||
Sometimes you will need to make a change in the gocui fork (https://github.com/jesseduffield/gocui). Gocui is the package responsible for rending windows and handling user input. Here's the typical process to follow:
|
||||
|
||||
1. Make the changes in gocui inside the vendor directory so it's easy to test against lazygit
|
||||
2. Copy the changes over to the actual gocui repo (clone it if you haven't already, and use the `awesome` branch, not `master`)
|
||||
3. Raise a PR on the gocui repo with your changes
|
||||
4. After that PR is merged, make a PR in lazygit bumping the gocui version. You can bump the version by running the following at the lazygit repo root:
|
||||
|
||||
```sh
|
||||
./bump_gocui.sh
|
||||
```
|
||||
|
||||
5. Raise a PR in lazygit with those changes
|
||||
|
||||
@@ -21,7 +21,6 @@ If you're a mere mortal like me and you're tired of hearing how powerful git is
|
||||
- [Binary releases](#binary-releases)
|
||||
- [Homebrew](#homebrew)
|
||||
- [MacPorts](#macports)
|
||||
- [Ubuntu](#ubuntu)
|
||||
- [Void Linux](#void-linux)
|
||||
- [Scoop (Windows)](#scoop-windows)
|
||||
- [Arch Linux](#arch-linux)
|
||||
@@ -53,7 +52,7 @@ Github Sponsors is matching all donations dollar-for-dollar for 12 months so if
|
||||
|
||||
### Binary Releases
|
||||
|
||||
For Windows, Mac OS(10.10+) or Linux, you can download a binary release [here](../../releases).
|
||||
For Windows, Mac OS(10.12+) or Linux, you can download a binary release [here](../../releases).
|
||||
|
||||
### Homebrew
|
||||
|
||||
@@ -82,6 +81,8 @@ sudo port install lazygit
|
||||
|
||||
### Ubuntu
|
||||
|
||||
**Deprecated**: will no longer receive updates.
|
||||
|
||||
Packages for Ubuntu are available via [Launchpad PPA](https://launchpad.net/~lazygit-team).
|
||||
|
||||
```sh
|
||||
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
# Go's proxy servers are not very up-to-date so that's why we use `GOPROXY=direct`
|
||||
# We specify the `awesome` branch to avoid the default behaviour of looking for a semver tag.
|
||||
GOPROXY=direct go get -u github.com/jesseduffield/gocui@awesome && go mod vendor && go mod tidy
|
||||
|
||||
# Note to self if you ever want to fork a repo be sure to use this same approach: it's important to use the branch name (e.g. master)
|
||||
+90
-6
@@ -22,6 +22,7 @@ gui:
|
||||
sidePanelWidth: 0.3333 # number from 0 to 1
|
||||
expandFocusedSidePanel: false
|
||||
mainPanelSplitMode: 'flexible' # one of 'horizontal' | 'flexible' | 'vertical'
|
||||
language: 'auto' # one of 'auto' | 'en' | 'zh' | 'pl' | 'nl'
|
||||
theme:
|
||||
lightTheme: false # For terminals with a light background
|
||||
activeBorderColor:
|
||||
@@ -35,15 +36,22 @@ gui:
|
||||
- default
|
||||
selectedRangeBgColor:
|
||||
- blue
|
||||
cherryPickedCommitBgColor:
|
||||
- blue
|
||||
cherryPickedCommitFgColor:
|
||||
- cyan
|
||||
commitLength:
|
||||
show: true
|
||||
mouseEvents: true
|
||||
skipUnstageLineWarning: false
|
||||
skipStashWarning: true
|
||||
showFileTree: false # for rendering changes files in a tree format
|
||||
showListFooter: true # for seeing the '5 of 20' message in list panels
|
||||
showRandomTip: true
|
||||
showCommandLog: true
|
||||
commandLogSize: 8
|
||||
authorColors: # in case you're not happy with the randomly assigned colour
|
||||
'John Smith': '#ff0000'
|
||||
git:
|
||||
paging:
|
||||
colorArg: always
|
||||
@@ -53,14 +61,25 @@ git:
|
||||
manualCommit: false
|
||||
# extra args passed to `git merge`, e.g. --no-ff
|
||||
args: ''
|
||||
pull:
|
||||
mode: 'merge' # one of 'merge' | 'rebase' | 'ff-only'
|
||||
log:
|
||||
# one of date-order, author-date-order, topo-order.
|
||||
# topo-order makes it easier to read the git log graph, but commits may not
|
||||
# appear chronologically. See https://git-scm.com/docs/git-log#_commit_ordering
|
||||
order: 'topo-order'
|
||||
# one of always, never, when-maximised
|
||||
# this determines whether the git graph is rendered in the commits panel
|
||||
showGraph: 'when-maximised'
|
||||
skipHookPrefix: WIP
|
||||
autoFetch: true
|
||||
branchLogCmd: 'git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --'
|
||||
allBranchesLogCmd: 'git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium'
|
||||
overrideGpg: false # prevents lazygit from spawning a separate process when using GPG
|
||||
disableForcePushing: false
|
||||
parseEmoji: false
|
||||
os:
|
||||
editCommand: '' # see 'Configuring File Editing' section
|
||||
editCommandTemplate: '{{editor}} {{filename}}'
|
||||
openCommand: ''
|
||||
refresher:
|
||||
refreshInterval: 10 # file/submodule refresh interval in seconds
|
||||
fetchInterval: 60 # re-fetch interval in seconds
|
||||
@@ -88,16 +107,20 @@ keybinding:
|
||||
nextPage: '.' # go to previous page in list
|
||||
gotoTop: '<' # go to top of list
|
||||
gotoBottom: '>' # go to bottom of list
|
||||
scrollLeft: 'H' # scroll left within list view
|
||||
scrollRight: 'L' # scroll right within list view
|
||||
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
|
||||
jumpToBlock: ['1', '2', '3', '4', '5'] # goto the Nth block / panel
|
||||
nextMatch: 'n'
|
||||
prevMatch: 'N'
|
||||
optionMenu: 'x' # show help menu
|
||||
optionMenu-alt1: '?' # show help menu
|
||||
select: '<space>'
|
||||
goInto: '<enter>'
|
||||
openRecentRepos: '<c-r>'
|
||||
confirm: '<enter>'
|
||||
confirm-alt1: 'y'
|
||||
remove: 'd'
|
||||
@@ -127,7 +150,9 @@ keybinding:
|
||||
diffingMenu-alt: '<c-e>' # deprecated
|
||||
copyToClipboard: '<c-o>'
|
||||
submitEditorText: '<enter>'
|
||||
appendNewline: '<tab>'
|
||||
appendNewline: '<a-enter>'
|
||||
extrasMenu: '@'
|
||||
toggleWhitespaceInDiffView: '<c-w>'
|
||||
status:
|
||||
checkForUpdate: 'u'
|
||||
recentRepos: '<enter>'
|
||||
@@ -146,6 +171,7 @@ keybinding:
|
||||
toggleTreeView: '`'
|
||||
branches:
|
||||
createPullRequest: 'o'
|
||||
viewPullRequestOptions: 'O'
|
||||
checkoutBranchByName: 'c'
|
||||
forceCheckoutBranch: 'F'
|
||||
rebaseBranch: 'r'
|
||||
@@ -175,6 +201,7 @@ keybinding:
|
||||
checkoutCommit: '<space>'
|
||||
resetCherryPick: '<c-R>'
|
||||
copyCommitMessageToClipboard: '<c-y>'
|
||||
openLogMenu: '<c-l>'
|
||||
stash:
|
||||
popStash: 'g'
|
||||
commitFiles:
|
||||
@@ -196,14 +223,14 @@ keybinding:
|
||||
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'cmd /c "start "" {{filename}}"'
|
||||
openCommand: 'start "" {{filename}}'
|
||||
```
|
||||
|
||||
### Linux
|
||||
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'
|
||||
openCommand: 'xdg-open {{filename}} >/dev/null'
|
||||
```
|
||||
|
||||
### OSX
|
||||
@@ -213,6 +240,57 @@ os:
|
||||
openCommand: 'open {{filename}}'
|
||||
```
|
||||
|
||||
### Configuring File Editing
|
||||
|
||||
Lazygit will edit a file with the first set editor in the following:
|
||||
|
||||
1. config.yaml
|
||||
|
||||
```yaml
|
||||
os:
|
||||
editCommand: 'vim' # as an example
|
||||
```
|
||||
|
||||
2. \$(git config core.editor)
|
||||
3. \$GIT_EDITOR
|
||||
4. \$VISUAL
|
||||
5. \$EDITOR
|
||||
6. \$(which vi)
|
||||
|
||||
Lazygit will log an error if none of these options are set.
|
||||
|
||||
You can specify a line number you are currently at when in the line-by-line mode.
|
||||
|
||||
```yaml
|
||||
os:
|
||||
editCommand: 'vim'
|
||||
editCommandTemplate: '{{editor}} +{{line}} {{filename}}'
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```yaml
|
||||
os:
|
||||
editCommand: 'code'
|
||||
editCommandTemplate: '{{editor}} --goto {{filename}}:{{line}}'
|
||||
```
|
||||
|
||||
`{{editor}}` in `editCommandTemplate` is replaced with the value of `editCommand`.
|
||||
|
||||
### Overriding default config file location
|
||||
|
||||
To override the default config directory, use `$CONFIG_DIR="~/.config/lazygit"`. This directory contains the config file in addition to some other files lazygit uses to keep track of state across sessions.
|
||||
|
||||
To override the individual config file used, use the `--use-config-file` arg or the `LG_CONFIG_FILE` env var.
|
||||
|
||||
If you want to merge a specific config file into a more general config file, perhaps for the sake of setting some theme-specific options, you can supply a list of comma-separated config file paths, like so:
|
||||
|
||||
```sh
|
||||
lazygit --use-config-file=~/.base_lg_conf,~/.light_theme_lg_conf
|
||||
or
|
||||
LG_CONFIG_FILE="~/.base_lg_conf,~/.light_theme_lg_conf" lazygit
|
||||
```
|
||||
|
||||
### Recommended Config Values
|
||||
|
||||
for users of VSCode
|
||||
@@ -227,7 +305,8 @@ os:
|
||||
For color attributes you can choose an array of attributes (with max one color attribute)
|
||||
The available attributes are:
|
||||
|
||||
- default
|
||||
**Colors**
|
||||
|
||||
- black
|
||||
- red
|
||||
- green
|
||||
@@ -236,7 +315,12 @@ The available attributes are:
|
||||
- magenta
|
||||
- cyan
|
||||
- white
|
||||
- '#ff00ff'
|
||||
|
||||
**Modifiers**
|
||||
|
||||
- bold
|
||||
- default
|
||||
- reverse # useful for high-contrast
|
||||
- underline
|
||||
|
||||
|
||||
@@ -35,6 +35,20 @@ customCommands:
|
||||
command: "git flow {{index .PromptResponses 0}} start {{index .PromptResponses 1}}"
|
||||
context: 'localBranches'
|
||||
loadingText: 'creating branch'
|
||||
- key : 'r'
|
||||
description: 'Checkout a remote branch as FETCH_HEAD'
|
||||
command: "git fetch {{index .PromptResponses 0}} {{index .PromptResponses 1}} && git checkout FETCH_HEAD"
|
||||
context: 'remotes'
|
||||
prompts:
|
||||
- type: 'input'
|
||||
title: 'Remote:'
|
||||
initialValue: "{{index .SelectedRemote.Name }}"
|
||||
- type: 'menuFromCommand'
|
||||
title: 'Remote branch:'
|
||||
command: 'git branch -r --list {{index .PromptResponses 0}}/*'
|
||||
filter: '.*{{index .PromptResponses 0}}/(?P<branch>.*)'
|
||||
valueFormat: '{{ .branch }}'
|
||||
labelFormat: '{{ .branch | green }}'
|
||||
```
|
||||
|
||||
Looking at the command assigned to the 'n' key, here's what the result looks like:
|
||||
@@ -79,12 +93,28 @@ The permitted contexts are:
|
||||
|
||||
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 |
|
||||
| _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 |
|
||||
| command | (only applicable to 'menuFromCommand' prompts) the command to run to generate | yes |
|
||||
| | menu options | |
|
||||
| filter | (only applicable to 'menuFromCommand' prompts) the regexp to run specifying | yes |
|
||||
| | groups which are going to be kept from the command's output | |
|
||||
| valueFormat | (only applicable to 'menuFromCommand' prompts) how to format matched groups from | yes |
|
||||
| | the filter to construct a menu item's value (What gets appended to prompt | |
|
||||
| | responses when the item is selected). You can use named groups, | |
|
||||
| | or `{{ .group_GROUPID }}`. | |
|
||||
| | PS: named groups keep first match only | |
|
||||
| labelFormat | (only applicable to 'menuFromCommand' prompts) how to format matched groups from | no |
|
||||
| | the filter to construct the item's label (What's shown on screen). You can use | |
|
||||
| | named groups, or `{{ .group_GROUPID }}`. You can also color each match with | |
|
||||
| | `{{ .group_GROUPID | colorname }}` (Color names from | |
|
||||
| | [here](https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md)) | |
|
||||
| | If `labelFormat` is not specified, `valueFormat` is shown instead. | |
|
||||
| | PS: named groups keep first match only | |
|
||||
|
||||
The permitted option fields are:
|
||||
| _field_ | _description_ | _required_ |
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
## Global Keybindings
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+r</kbd>: switch to a recent repo (<c-r>)
|
||||
<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
|
||||
@@ -16,9 +17,10 @@
|
||||
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: prev screen mode
|
||||
<kbd>:</kbd>: execute custom command
|
||||
<kbd>|</kbd>: view filter-by-path options
|
||||
<kbd>ctrl+s</kbd>: view filter-by-path options
|
||||
<kbd>W</kbd>: open diff menu
|
||||
<kbd>ctrl+e</kbd>: open diff menu
|
||||
<kbd>@</kbd>: open command log menu
|
||||
</pre>
|
||||
|
||||
## List Panel Navigation
|
||||
@@ -38,6 +40,7 @@
|
||||
<pre>
|
||||
<kbd>space</kbd>: checkout
|
||||
<kbd>o</kbd>: create pull request
|
||||
<kbd>O</kbd>: create pull request options
|
||||
<kbd>ctrl+y</kbd>: copy pull request URL to clipboard
|
||||
<kbd>c</kbd>: checkout by name
|
||||
<kbd>F</kbd>: force checkout
|
||||
@@ -122,7 +125,7 @@
|
||||
<kbd>g</kbd>: reset to this commit
|
||||
<kbd>f</kbd>: fixup commit
|
||||
<kbd>F</kbd>: create fixup commit for this commit
|
||||
<kbd>S</kbd>: squash above commits
|
||||
<kbd>S</kbd>: squash all 'fixup!' commits above selected commit (autosquash)
|
||||
<kbd>d</kbd>: delete commit
|
||||
<kbd>ctrl+j</kbd>: move commit down one
|
||||
<kbd>ctrl+k</kbd>: move commit up one
|
||||
@@ -154,6 +157,12 @@
|
||||
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||
</pre>
|
||||
|
||||
## Extras Panel
|
||||
|
||||
<pre>
|
||||
<kbd>@</kbd>: open command log menu
|
||||
</pre>
|
||||
|
||||
## Files Panel (Files)
|
||||
|
||||
<pre>
|
||||
@@ -176,6 +185,8 @@
|
||||
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
|
||||
<kbd>g</kbd>: view upstream reset options
|
||||
<kbd>`</kbd>: toggle file tree view
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
|
||||
</pre>
|
||||
|
||||
## Files Panel (Submodules)
|
||||
@@ -195,12 +206,13 @@
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: return to files panel
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>space</kbd>: pick hunk
|
||||
<kbd>b</kbd>: pick both hunks
|
||||
<kbd>b</kbd>: pick all hunks
|
||||
<kbd>◄</kbd>: select previous conflict
|
||||
<kbd>►</kbd>: select next conflict
|
||||
<kbd>▲</kbd>: select top hunk
|
||||
<kbd>▼</kbd>: select bottom hunk
|
||||
<kbd>▲</kbd>: select previous hunk
|
||||
<kbd>▼</kbd>: select next hunk
|
||||
<kbd>z</kbd>: undo
|
||||
</pre>
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# Lazygit Sneltoetsen
|
||||
|
||||
## Globaale Sneltoetsen
|
||||
## Globale Sneltoetsen
|
||||
|
||||
<pre>
|
||||
<kbd>pgup</kbd>: scroll naar beneden vanaf hooft paneel (fn+up)
|
||||
<kbd>pgdown</kbd>: scroll naar beneden vabaf hooft paneel (fn+down)
|
||||
<kbd>ctrl+r</kbd>: wissel naar een recente repo (<c-r>)
|
||||
<kbd>pgup</kbd>: scroll naar beneden vanaf hoofdpaneel (fn+up)
|
||||
<kbd>pgdown</kbd>: scroll naar beneden vanaf hoofdpaneel (fn+down)
|
||||
<kbd>m</kbd>: bekijk merge/rebase opties
|
||||
<kbd>ctrl+p</kbd>: bekijk aangepaste patch opties
|
||||
<kbd>P</kbd>: push
|
||||
@@ -13,31 +14,33 @@
|
||||
<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>+</kbd>: volgende scherm modus (normaal/half/groot)
|
||||
<kbd>_</kbd>: vorige scherm modus
|
||||
<kbd>:</kbd>: voor aangepaste commando uit
|
||||
<kbd>ctrl+s</kbd>: bekijk scoping opties
|
||||
<kbd>W</kbd>: open diff menu
|
||||
<kbd>ctrl+e</kbd>: open diff menu
|
||||
<kbd>@</kbd>: open command log menu
|
||||
</pre>
|
||||
|
||||
## List Panel Navigation
|
||||
## Lijstpaneel Navigatie
|
||||
|
||||
<pre>
|
||||
<kbd>.</kbd>: volgende pagina
|
||||
<kbd>,</kbd>: vorige pagina
|
||||
<kbd><</kbd>: scroll naar boven
|
||||
<kbd>></kbd>: scroll naar beneden
|
||||
<kbd>/</kbd>: start met zoekken
|
||||
<kbd>]</kbd>: volgende tab
|
||||
<kbd>[</kbd>: vorige tab
|
||||
<kbd>/</kbd>: start met zoeken
|
||||
<kbd>]</kbd>: volgende tabblad
|
||||
<kbd>[</kbd>: vorige tabblad
|
||||
</pre>
|
||||
|
||||
## Branches Paneel (Branches Tab)
|
||||
## Branches Paneel (Branches Tabblad)
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: uitchecken
|
||||
<kbd>o</kbd>: maak een pull-aanvraag
|
||||
<kbd>o</kbd>: maak een pull-request
|
||||
<kbd>O</kbd>: bekijk opties voor pull-aanvraag
|
||||
<kbd>ctrl+y</kbd>: kopieer de URL van het pull-verzoek naar het klembord
|
||||
<kbd>c</kbd>: uitchecken bij naam
|
||||
<kbd>F</kbd>: forceer checkout
|
||||
@@ -49,16 +52,16 @@
|
||||
<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
|
||||
<kbd>ctrl+o</kbd>: kopieer branch name naar klembord
|
||||
<kbd>enter</kbd>: bekijk commits
|
||||
</pre>
|
||||
|
||||
## Branches Paneel (Remote Branches (in Remotes tab))
|
||||
## Branches Paneel (Remote Branches (in Remotes tabblad))
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: Ga terug naar remotes lijst
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>enter</kbd>: view commits
|
||||
<kbd>enter</kbd>: bekijk commits
|
||||
<kbd>space</kbd>: uitchecken
|
||||
<kbd>n</kbd>: nieuwe branch
|
||||
<kbd>M</kbd>: merge in met huidige checked out branch
|
||||
@@ -67,7 +70,7 @@
|
||||
<kbd>u</kbd>: stel in als upstream van uitgecheckte branch
|
||||
</pre>
|
||||
|
||||
## Branches Paneel (Remotes Tab)
|
||||
## Branches Paneel (Remotes Tabblad)
|
||||
|
||||
<pre>
|
||||
<kbd>f</kbd>: fetch remote
|
||||
@@ -83,13 +86,13 @@
|
||||
<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
|
||||
<kbd>c</kbd>: kopieer commit (cherry-pick)
|
||||
<kbd>C</kbd>: kopieer commit reeks (cherry-pick)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
|
||||
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
|
||||
</pre>
|
||||
|
||||
## Branches Paneel (Tags Tab)
|
||||
## Branches Paneel (Tags Tabblad)
|
||||
|
||||
<pre>
|
||||
<kbd>space</kbd>: uitchecken
|
||||
@@ -97,7 +100,7 @@
|
||||
<kbd>P</kbd>: push tag
|
||||
<kbd>n</kbd>: creëer tag
|
||||
<kbd>g</kbd>: bekijk reset opties
|
||||
<kbd>enter</kbd>: view commits
|
||||
<kbd>enter</kbd>: bekijk commits
|
||||
</pre>
|
||||
|
||||
## Commit bestanden Paneel
|
||||
@@ -109,8 +112,8 @@
|
||||
<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
|
||||
<kbd>`</kbd>: toggle file tree view
|
||||
<kbd>enter</kbd>: enter bestand om geselecteerde regels toe te voegen aan de patch
|
||||
<kbd>`</kbd>: toggle bestandsboom weergave
|
||||
</pre>
|
||||
|
||||
## Commits Paneel (Commits)
|
||||
@@ -130,28 +133,34 @@
|
||||
<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>c</kbd>: kopieer commit (cherry-pick)
|
||||
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
|
||||
<kbd>C</kbd>: kopieer 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>n</kbd>: creëer nieuwe branch van commit
|
||||
<kbd>T</kbd>: tag commit
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (gecopieerde) commits selectie
|
||||
<kbd>ctrl+y</kbd>: copieer commit bericht naar clipboard
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
|
||||
<kbd>ctrl+y</kbd>: kopieer commit bericht naar klembord
|
||||
</pre>
|
||||
|
||||
## Commits Paneel (Reflog Tab)
|
||||
## Commits Paneel (Reflog Tabblad)
|
||||
|
||||
<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
|
||||
<kbd>c</kbd>: kopieer commit (cherry-pick)
|
||||
<kbd>C</kbd>: kopieer commit reeks (cherry-pick)
|
||||
<kbd>ctrl+r</kbd>: reset cherry-picked (gekopieerde) commits selectie
|
||||
<kbd>ctrl+o</kbd>: kopieer commit SHA naar klembord
|
||||
</pre>
|
||||
|
||||
## Extras Paneel
|
||||
|
||||
<pre>
|
||||
<kbd>@</kbd>: open command log menu
|
||||
</pre>
|
||||
|
||||
## Bestanden Paneel (Bestanden)
|
||||
@@ -175,26 +184,29 @@
|
||||
<kbd>f</kbd>: fetch
|
||||
<kbd>ctrl+o</kbd>: kopieer de bestandsnaam naar het klembord
|
||||
<kbd>g</kbd>: bekijk upstream reset opties
|
||||
<kbd>`</kbd>: toggle file tree view
|
||||
<kbd>`</kbd>: toggle bestandsboom weergave
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
|
||||
</pre>
|
||||
|
||||
## Bestanden Paneel (Submodules)
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+o</kbd>: copy submodule name to clipboard
|
||||
<kbd>ctrl+o</kbd>: kopieer submodule naam naar klembord
|
||||
<kbd>enter</kbd>: enter submodule
|
||||
<kbd>d</kbd>: view reset and remove submodule options
|
||||
<kbd>d</kbd>: bekijk reset en verwijder submodule opties
|
||||
<kbd>u</kbd>: update submodule
|
||||
<kbd>n</kbd>: add new submodule
|
||||
<kbd>n</kbd>: voeg nieuwe submodule toe
|
||||
<kbd>e</kbd>: update submodule URL
|
||||
<kbd>i</kbd>: initialize submodule
|
||||
<kbd>b</kbd>: view bulk submodule options
|
||||
<kbd>i</kbd>: initialiseer submodule
|
||||
<kbd>b</kbd>: bekijk bulk submodule opties
|
||||
</pre>
|
||||
|
||||
## Hooft Paneel (Merggen)
|
||||
## Hoofd Paneel (Mergen)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: ga terug naar het bestanden paneel
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>space</kbd>: kies hunk
|
||||
<kbd>b</kbd>: kies bijde hunks
|
||||
<kbd>◄</kbd>: selecteer voorgaand conflict
|
||||
@@ -204,29 +216,29 @@
|
||||
<kbd>z</kbd>: ongedaan maken
|
||||
</pre>
|
||||
|
||||
## Hooft Paneel (Normaal)
|
||||
## Hoofd Paneel (Normaal)
|
||||
|
||||
<pre>
|
||||
<kbd>Ő</kbd>: scroll omlaag (fn+up)
|
||||
<kbd>ő</kbd>: scroll omhoog (fn+down)
|
||||
</pre>
|
||||
|
||||
## Hooft Paneel (Patch Bouwen)
|
||||
## Hoofd Paneel (Patch Bouwen)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: sluit lijn-bij-lijn mode
|
||||
<kbd>esc</kbd>: sluit lijn-bij-lijn modus
|
||||
<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
|
||||
<kbd>v</kbd>: toggle drag selecteer
|
||||
<kbd>V</kbd>: toggle drag selecteer
|
||||
<kbd>a</kbd>: toggle selecteer hunk
|
||||
</pre>
|
||||
|
||||
## Hooft Paneel (Staging)
|
||||
## Hoofd Paneel (Staging)
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: ga terug naar het bestanden paneel
|
||||
@@ -240,9 +252,9 @@
|
||||
<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>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
|
||||
@@ -257,7 +269,7 @@
|
||||
## Stash Paneel
|
||||
|
||||
<pre>
|
||||
<kbd>enter</kbd>: view stash entry's files
|
||||
<kbd>enter</kbd>: bekijk bestanden van stash entry
|
||||
<kbd>space</kbd>: toepassen
|
||||
<kbd>g</kbd>: pop
|
||||
<kbd>d</kbd>: laten vallen
|
||||
@@ -271,5 +283,5 @@
|
||||
<kbd>o</kbd>: open config bestand
|
||||
<kbd>u</kbd>: check voor updates
|
||||
<kbd>enter</kbd>: wissel naar een recente repo
|
||||
<kbd>a</kbd>: alle takken van het houtblok laten zien
|
||||
<kbd>a</kbd>: alle logs van de branch laten zien
|
||||
</pre>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
## Globalne
|
||||
|
||||
<pre>
|
||||
<kbd>ctrl+r</kbd>: switch to a recent repo (<c-r>)
|
||||
<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
|
||||
@@ -16,9 +17,10 @@
|
||||
<kbd>+</kbd>: next screen mode (normal/half/fullscreen)
|
||||
<kbd>_</kbd>: prev screen mode
|
||||
<kbd>:</kbd>: execute custom command
|
||||
<kbd>|</kbd>: view filter-by-path options
|
||||
<kbd>ctrl+s</kbd>: view filter-by-path options
|
||||
<kbd>W</kbd>: open diff menu
|
||||
<kbd>ctrl+e</kbd>: open diff menu
|
||||
<kbd>@</kbd>: open command log menu
|
||||
</pre>
|
||||
|
||||
## List Panel Navigation
|
||||
@@ -38,6 +40,7 @@
|
||||
<pre>
|
||||
<kbd>space</kbd>: przełącz
|
||||
<kbd>o</kbd>: utwórz żądanie wyciągnięcia
|
||||
<kbd>O</kbd>: utwórz opcje żądania ściągnięcia
|
||||
<kbd>ctrl+y</kbd>: skopiuj adres URL żądania ściągnięcia do schowka
|
||||
<kbd>c</kbd>: przełącz używając nazwy
|
||||
<kbd>F</kbd>: wymuś przełączenie
|
||||
@@ -122,7 +125,7 @@
|
||||
<kbd>g</kbd>: zresetuj do tego commita
|
||||
<kbd>f</kbd>: napraw commit
|
||||
<kbd>F</kbd>: create fixup commit for this commit
|
||||
<kbd>S</kbd>: squash above commits
|
||||
<kbd>S</kbd>: squash all 'fixup!' commits above selected commits (autosquash)
|
||||
<kbd>d</kbd>: delete commit
|
||||
<kbd>ctrl+j</kbd>: move commit down one
|
||||
<kbd>ctrl+k</kbd>: move commit up one
|
||||
@@ -154,6 +157,12 @@
|
||||
<kbd>ctrl+o</kbd>: copy commit SHA to clipboard
|
||||
</pre>
|
||||
|
||||
## Extras Panel
|
||||
|
||||
<pre>
|
||||
<kbd>@</kbd>: open command log menu
|
||||
</pre>
|
||||
|
||||
## Pliki Panel (Pliki)
|
||||
|
||||
<pre>
|
||||
@@ -176,6 +185,8 @@
|
||||
<kbd>ctrl+o</kbd>: copy the file name to the clipboard
|
||||
<kbd>g</kbd>: view upstream reset options
|
||||
<kbd>`</kbd>: toggle file tree view
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>ctrl+w</kbd>: Toggle whether or not whitespace changes are shown in the diff view
|
||||
</pre>
|
||||
|
||||
## Pliki Panel (Submodules)
|
||||
@@ -195,12 +206,13 @@
|
||||
|
||||
<pre>
|
||||
<kbd>esc</kbd>: wróć do panelu plików
|
||||
<kbd>M</kbd>: open external merge tool (git mergetool)
|
||||
<kbd>space</kbd>: pick hunk
|
||||
<kbd>b</kbd>: pick both hunks
|
||||
<kbd>b</kbd>: pick all hunks
|
||||
<kbd>◄</kbd>: select previous conflict
|
||||
<kbd>►</kbd>: select next conflict
|
||||
<kbd>▲</kbd>: select top hunk
|
||||
<kbd>▼</kbd>: select bottom hunk
|
||||
<kbd>▲</kbd>: select previous hunk
|
||||
<kbd>▼</kbd>: select next hunk
|
||||
<kbd>z</kbd>: cofnij
|
||||
</pre>
|
||||
|
||||
|
||||
@@ -9,39 +9,39 @@ require (
|
||||
github.com/cli/safeexec v1.0.0
|
||||
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.9.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.4.7
|
||||
github.com/gdamore/tcell/v2 v2.2.0 // indirect
|
||||
github.com/go-errors/errors v1.1.1
|
||||
github.com/go-errors/errors v1.4.1
|
||||
github.com/go-logfmt/logfmt v0.5.0 // indirect
|
||||
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/google/go-cmp v0.5.6 // indirect
|
||||
github.com/gookit/color v1.4.2
|
||||
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.20210417110745-37f79434200d
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe // indirect
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20211102104458-40df0be5a474
|
||||
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
|
||||
github.com/jesseduffield/yaml v2.1.0+incompatible
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.7 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.12
|
||||
github.com/kyokomi/emoji/v2 v2.2.8
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0
|
||||
github.com/mattn/go-colorable v0.1.11 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13
|
||||
github.com/mgutz/str v1.2.0
|
||||
github.com/onsi/ginkgo v1.10.3 // indirect
|
||||
github.com/onsi/gomega v1.7.1 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/sahilm/fuzzy v0.1.0
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
|
||||
github.com/stretchr/testify v1.4.0
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
|
||||
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-20210415045647-66c3f260301c // indirect
|
||||
golang.org/x/term v0.0.0-20210406210042-72f3dc4e9b72 // indirect
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
golang.org/x/sys v0.0.0-20211102061401-a2f17f7b995c // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0
|
||||
)
|
||||
|
||||
replace github.com/go-git/go-git/v5 => github.com/jesseduffield/go-git/v5 v5.1.1
|
||||
|
||||
@@ -23,7 +23,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emirpasic/gods v1.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=
|
||||
@@ -33,38 +32,36 @@ github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell/v2 v2.0.0 h1:GRWG8aLfWAlekj9Q6W29bVvkHENc6hp79XOqG4AWDOs=
|
||||
github.com/gdamore/tcell/v2 v2.0.0/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
|
||||
github.com/gdamore/tcell/v2 v2.1.0 h1:UnSmozHgBkQi2PGsFr+rpdXuAPRRucMegpQp3Z3kDro=
|
||||
github.com/gdamore/tcell/v2 v2.1.0/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
|
||||
github.com/gdamore/tcell/v2 v2.2.0 h1:vSyEgKwraXPSOkvCk7IwOSyX+Pv3V2cV9CikJMXg4U4=
|
||||
github.com/gdamore/tcell/v2 v2.2.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
|
||||
github.com/gdamore/tcell/v2 v2.4.0 h1:W6dxJEmaxYvhICFoTY3WrLLEXsQ11SaFnKGVEXW57KM=
|
||||
github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
|
||||
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
|
||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
|
||||
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-errors/errors v1.4.1 h1:IvVlgbzSsaUNudsw5dcXSzF3EWyXTi5XrAdngnuhRyg=
|
||||
github.com/go-errors/errors v1.4.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
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-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/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 h1:zN2lZNZRflqFyxVaTIU61KNKQ9C0055u9CAfpmqUvo4=
|
||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3/go.mod h1:nPpo7qLxd6XL3hWJG/O60sR8ZKfMCiIoNap5GvD12KU=
|
||||
github.com/golang/protobuf v1.2.0/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/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/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/gookit/color v1.4.2 h1:tXy44JFSFkKnELV6WaMo/lLfu/meqITX3iAV52do7lk=
|
||||
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
|
||||
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=
|
||||
@@ -74,54 +71,10 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl
|
||||
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.20161105104656-d666c9f652af h1:9ZI/QyVOerYYeqMt4svycU2Lz0WvxNHCpHHbsFsi/oA=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20161105104656-d666c9f652af/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
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/gocui v0.3.1-0.20201224041937-f5a9733d1860 h1:1xfQM6T5A4jqcVvUnYaKR6bGrOhDLWQsp79JFNJpzcQ=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20201224041937-f5a9733d1860/go.mod h1:9LmtJcK+Kwiuc2huslzS37uFJPdHka2Cs/cQ06JZdbk=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210208224444-2eecee85583d h1:Jto9W9w8CFwZiAYXa7LsHDEOb5cKCA1f5LOL1A3jva4=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210208224444-2eecee85583d/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329125022-96b1d3106429 h1:Ih3UVczKRabZnQ7RisGi5uItC2QJxdqgef7AClJ2G9A=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329125022-96b1d3106429/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329125502-e830abf4b73a h1:RVYf2MA/RJbodE+S0e2z++JmB9A7hD1lUsI0euv1fmA=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329125502-e830abf4b73a/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329130738-e026850021e3 h1:UDiArPlzkg+8mmNjhUOamQoyiTSzQUGIpOsu5hCRJVI=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329130738-e026850021e3/go.mod h1:2RtZznzYKt8RLRwvFiSkXjU0Ei8WwHdubgnlaYH47dw=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329131148-bcc4dcd991ff h1:fTt3EzLtpsc7OA7A6Vd6JVnlxvcAy7cY9lmN9yzDwSs=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210329131148-bcc4dcd991ff/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210402033412-1238f910f001 h1:1WH+lTSK5YMr8emISHPA+VqYDDcLei6djuSxBCLIaiI=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210402033412-1238f910f001/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210402040718-77a1b9631715 h1:nELTdFJiZk3vv7j8nWoHvl7H2IqTr26EHKl6LaorRA8=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210402040718-77a1b9631715/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210402113210-6fd7ef27ce76 h1:miXVlortFNTlOX+KiKW3cVxOR6+Uhl4pnQRei2X26Y4=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210402113210-6fd7ef27ce76/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210403045716-a3be78c4ccf6 h1:nENhj0TKu+11RrPm9Ls5YtzkpbNHM0faXr9UECDhODQ=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210403045716-a3be78c4ccf6/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210405041826-439abd8b6e07 h1:BymGR28auSeuW0QELl0JomK0iFLPS/WRjFlc1iGZiOQ=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210405041826-439abd8b6e07/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210405093708-e79dab8f7772 h1:dg9krj10Udac4IcvlVCOAPktQkfggkgtqRmbDKk7Pzw=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210405093708-e79dab8f7772/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210406065811-95ef6e13779b h1:3+4+muhhikpls5FePXSRNFgcdoPx8dTdqaCy3AqLz98=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210406065811-95ef6e13779b/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210406065942-1b0c68414064 h1:Oe+QJuUIOd2TU+A3BW5sT1eXqceoBcOOfyoHlGf7F8Y=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210406065942-1b0c68414064/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210409121040-210802112d8a h1:ocrSuZxQIgWWt27b+rjiyIIPz6fzfFeoL5Q4cpa2cAo=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210409121040-210802112d8a/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210410011117-a2bb4baca390 h1:Es72JiUjt01TtvqCugdvOR91baB3DhuWF1DNuxA0frA=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210410011117-a2bb4baca390/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210412111008-6ef019af3724 h1:U70Do3/OSw5n/oLJGPWsQHnos2p0yq8yAeD2muioJhQ=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210412111008-6ef019af3724/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210412113212-ee65bd542c08 h1:d003y2GByfR3PqN/JvxNuqyo8vx4m0epwY2hW7sNU80=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210412113212-ee65bd542c08/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210412130453-de7bb5079f9f h1:JPpHlvSrKNxro+K9rM3nEHCdZ16qD0hnEedHPF07OtA=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210412130453-de7bb5079f9f/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210417105214-bdf37de5c917 h1:H4THGOdAJf61wByuq8EHF/NAgtqrTxpSIPsrCXU9HAY=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210417105214-bdf37de5c917/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210417110745-37f79434200d h1:2BPcc19W0j576hvhxtKma4jcD/+qAYvw1ln2HcIEZGU=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210417110745-37f79434200d/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
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/gocui v0.3.1-0.20211102104458-40df0be5a474 h1:4H/oJcUmwJpqyXzqfn+lsjQ/bjpm/HszzLrVbCjgqj4=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20211102104458-40df0be5a474/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
|
||||
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e h1:uw/oo+kg7t/oeMs6sqlAwr85ND/9cpO3up3VxphxY0U=
|
||||
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e/go.mod h1:u60qdFGXRd36jyEXxetz0vQceQIxzI13lIo3EFUDf4I=
|
||||
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/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
@@ -134,37 +87,30 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3
|
||||
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/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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
||||
github.com/kyokomi/emoji/v2 v2.2.8 h1:jcofPxjHWEkJtkIbcLHvZhxKgCPl6C7MyjTrD4KDqUE=
|
||||
github.com/kyokomi/emoji/v2 v2.2.8/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
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-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
|
||||
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
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.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
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.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
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=
|
||||
@@ -180,7 +126,6 @@ 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/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
@@ -196,21 +141,24 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
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/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/urfave/cli v1.20.1-0.20180226030253-8e01ec4cd3e2/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
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/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
||||
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/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
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=
|
||||
@@ -222,47 +170,34 @@ golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
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-20190626150813-e07cf5db2756/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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210217105451-b926d437f341 h1:2/QtM1mL37YmcsT8HaDNHDgTqqFVw+zr8UzMiBVLzYU=
|
||||
golang.org/x/sys v0.0.0-20210217105451-b926d437f341/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54 h1:rF3Ohx8DRyl8h2zw9qojyLHLhrJpEMgyPOImREEryf0=
|
||||
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210402192133-700132347e07 h1:4k6HsQjxj6hVMsI2Vf0yKlzt5lXxZsMW1q0zaq2k8zY=
|
||||
golang.org/x/sys v0.0.0-20210402192133-700132347e07/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210415045647-66c3f260301c h1:6L+uOeS3OQt/f4eFHXZcTxeZrGCuz+CLElgEBjbcTA4=
|
||||
golang.org/x/sys v0.0.0-20210415045647-66c3f260301c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211102061401-a2f17f7b995c h1:QOfDMdrf/UwlVR0UBq2Mpr58UzNtvgJRXA4BgPfFACs=
|
||||
golang.org/x/sys v0.0.0-20211102061401-a2f17f7b995c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 h1:EC6+IGYTjPpRfv9a2b/6Puw0W+hLtAhkV1tPsXhutqs=
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210406210042-72f3dc4e9b72 h1:VqE9gduFZ4dbR7XoL77lHFp0/DyDUBKSXK7CMFkVcV0=
|
||||
golang.org/x/term v0.0.0-20210406210042-72f3dc4e9b72/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
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/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/ozeidan/fuzzy-patricia.v3 v3.0.0 h1:KzcWKJ0nMAmGoBhYVMnkWc1rXjB42lKy5aIys4TdLOA=
|
||||
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0/go.mod h1:XoytMOotjRRJVkIsQdxsPIioRLYFISEaY9a4tftOXAo=
|
||||
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=
|
||||
@@ -271,3 +206,5 @@ 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.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -61,6 +61,9 @@ func main() {
|
||||
gitDir := ""
|
||||
flaggy.String(&gitDir, "g", "git-dir", "equivalent of the --git-dir git argument")
|
||||
|
||||
customConfig := ""
|
||||
flaggy.String(&customConfig, "ucf", "use-config-file", "Comma seperated list to custom config file(s)")
|
||||
|
||||
flaggy.Parse()
|
||||
|
||||
if repoPath != "" {
|
||||
@@ -72,6 +75,10 @@ func main() {
|
||||
gitDir = filepath.Join(repoPath, ".git")
|
||||
}
|
||||
|
||||
if customConfig != "" {
|
||||
os.Setenv("LG_CONFIG_FILE", customConfig)
|
||||
}
|
||||
|
||||
if useConfigDir != "" {
|
||||
os.Setenv("CONFIG_DIR", useConfigDir)
|
||||
}
|
||||
|
||||
+16
-21
@@ -15,12 +15,12 @@ import (
|
||||
|
||||
"github.com/aybabtme/humanlog"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
"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/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/updates"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -104,7 +104,10 @@ func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
|
||||
}
|
||||
var err error
|
||||
app.Log = newLogger(config)
|
||||
app.Tr = i18n.NewTranslationSet(app.Log)
|
||||
app.Tr, err = i18n.NewTranslationSetFromConfig(app.Log, config.GetUserConfig().Gui.Language)
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
|
||||
// if we are being called in 'demon' mode, we can just return here
|
||||
app.ClientContext = os.Getenv("LAZYGIT_CLIENT_COMMAND")
|
||||
@@ -124,7 +127,13 @@ func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
|
||||
return app, err
|
||||
}
|
||||
|
||||
app.GitCommand, err = commands.NewGitCommand(app.Log, app.OSCommand, app.Tr, app.Config)
|
||||
app.GitCommand, err = commands.NewGitCommand(
|
||||
app.Log,
|
||||
app.OSCommand,
|
||||
app.Tr,
|
||||
app.Config,
|
||||
git_config.NewStdCachedGitConfig(app.Log),
|
||||
)
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
@@ -318,6 +327,9 @@ func TailLogs() {
|
||||
|
||||
fmt.Printf("Tailing log file %s\n\n", logFilePath)
|
||||
|
||||
opts := humanlog.DefaultOptions
|
||||
opts.Truncates = false
|
||||
|
||||
_, err = os.Stat(logFilePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
@@ -326,22 +338,5 @@ func TailLogs() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
cmd := secureexec.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)
|
||||
TailLogsForPlatform(logFilePath, opts)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/aybabtme/humanlog"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
func TailLogsForPlatform(logFilePath string, opts *humanlog.HandlerOptions) {
|
||||
cmd := secureexec.Command("tail", "-f", logFilePath)
|
||||
|
||||
stdout, _ := cmd.StdoutPipe()
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"github.com/aybabtme/humanlog"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TailLogsForPlatform(logFilePath string, opts *humanlog.HandlerOptions) {
|
||||
var lastModified int64 = 0
|
||||
var lastOffset int64 = 0
|
||||
for {
|
||||
stat, err := os.Stat(logFilePath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if stat.ModTime().Unix() > lastModified {
|
||||
err = TailFrom(lastOffset, logFilePath, opts)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
lastOffset = stat.Size()
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func OpenAndSeek(filepath string, offset int64) (*os.File, error) {
|
||||
file, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = file.Seek(offset, 0)
|
||||
if err != nil {
|
||||
_ = file.Close()
|
||||
return nil, err
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func TailFrom(lastOffset int64, logFilePath string, opts *humanlog.HandlerOptions) error {
|
||||
file, err := OpenAndSeek(logFilePath, lastOffset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileScanner := bufio.NewScanner(file)
|
||||
var lines []string
|
||||
for fileScanner.Scan() {
|
||||
lines = append(lines, fileScanner.Text())
|
||||
}
|
||||
file.Close()
|
||||
lineCount := len(lines)
|
||||
lastTen := lines
|
||||
if lineCount > 10 {
|
||||
lastTen = lines[lineCount-10:]
|
||||
}
|
||||
for _, line := range lastTen {
|
||||
reader := strings.NewReader(line)
|
||||
if err := humanlog.Scanner(reader, os.Stdout, opts); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+12
-12
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
// NewBranch create new branch
|
||||
func (c *GitCommand) NewBranch(name string, base string) error {
|
||||
return c.RunCommand("git checkout -b %s %s", name, base)
|
||||
return c.RunCommand("git checkout -b %s %s", c.OSCommand.Quote(name), c.OSCommand.Quote(base))
|
||||
}
|
||||
|
||||
// CurrentBranchName get the current branch name and displayname.
|
||||
@@ -47,7 +47,7 @@ func (c *GitCommand) DeleteBranch(branch string, force bool) error {
|
||||
command = "git branch -D"
|
||||
}
|
||||
|
||||
return c.OSCommand.RunCommand("%s %s", command, branch)
|
||||
return c.OSCommand.RunCommand("%s %s", command, c.OSCommand.Quote(branch))
|
||||
}
|
||||
|
||||
// Checkout checks out a branch (or commit), with --force if you set the force arg to true
|
||||
@@ -61,7 +61,7 @@ func (c *GitCommand) Checkout(branch string, options CheckoutOptions) error {
|
||||
if options.Force {
|
||||
forceArg = " --force"
|
||||
}
|
||||
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git checkout%s %s", forceArg, branch), oscommands.RunCommandOptions{EnvVars: options.EnvVars})
|
||||
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git checkout%s %s", forceArg, c.OSCommand.Quote(branch)), oscommands.RunCommandOptions{EnvVars: options.EnvVars})
|
||||
}
|
||||
|
||||
// GetBranchGraph gets the color-formatted graph of the log for the given branch
|
||||
@@ -73,24 +73,24 @@ func (c *GitCommand) GetBranchGraph(branchName string) (string, error) {
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetUpstreamForBranch(branchName string) (string, error) {
|
||||
output, err := c.RunCommandWithOutput("git rev-parse --abbrev-ref --symbolic-full-name %s@{u}", branchName)
|
||||
output, err := c.RunCommandWithOutput("git rev-parse --abbrev-ref --symbolic-full-name %s@{u}", c.OSCommand.Quote(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,
|
||||
"branchName": c.OSCommand.Quote(branchName),
|
||||
}
|
||||
return utils.ResolvePlaceholderString(branchLogCmdTemplate, templateValues)
|
||||
}
|
||||
|
||||
func (c *GitCommand) SetUpstreamBranch(upstream string) error {
|
||||
return c.RunCommand("git branch -u %s", upstream)
|
||||
return c.RunCommand("git branch -u %s", c.OSCommand.Quote(upstream))
|
||||
}
|
||||
|
||||
func (c *GitCommand) SetBranchUpstream(remoteName string, remoteBranchName string, branchName string) error {
|
||||
return c.RunCommand("git branch --set-upstream-to=%s/%s %s", remoteName, remoteBranchName, branchName)
|
||||
return c.RunCommand("git branch --set-upstream-to=%s/%s %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(remoteBranchName), c.OSCommand.Quote(branchName))
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetCurrentBranchUpstreamDifferenceCount() (string, string) {
|
||||
@@ -124,7 +124,7 @@ type MergeOpts struct {
|
||||
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)
|
||||
command := fmt.Sprintf("git merge --no-edit %s %s", mergeArgs, c.OSCommand.Quote(branchName))
|
||||
if opts.FastForwardOnly {
|
||||
command = fmt.Sprintf("%s --ff-only", command)
|
||||
}
|
||||
@@ -144,18 +144,18 @@ func (c *GitCommand) IsHeadDetached() bool {
|
||||
|
||||
// ResetHardHead runs `git reset --hard`
|
||||
func (c *GitCommand) ResetHard(ref string) error {
|
||||
return c.RunCommand("git reset --hard " + ref)
|
||||
return c.RunCommand("git reset --hard " + c.OSCommand.Quote(ref))
|
||||
}
|
||||
|
||||
// ResetSoft runs `git reset --soft HEAD`
|
||||
func (c *GitCommand) ResetSoft(ref string) error {
|
||||
return c.RunCommand("git reset --soft " + ref)
|
||||
return c.RunCommand("git reset --soft " + c.OSCommand.Quote(ref))
|
||||
}
|
||||
|
||||
func (c *GitCommand) ResetMixed(ref string) error {
|
||||
return c.RunCommand("git reset --mixed " + ref)
|
||||
return c.RunCommand("git reset --mixed " + c.OSCommand.Quote(ref))
|
||||
}
|
||||
|
||||
func (c *GitCommand) RenameBranch(oldName string, newName string) error {
|
||||
return c.RunCommand("git branch --move %s %s", oldName, newName)
|
||||
return c.RunCommand("git branch --move %s %s", c.OSCommand.Quote(oldName), c.OSCommand.Quote(newName))
|
||||
}
|
||||
|
||||
@@ -47,6 +47,10 @@ func (c *GitCommand) GetCommitMessage(commitSha string) (string, error) {
|
||||
return strings.TrimSpace(message), err
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetCommitMessageFirstLine(sha string) (string, error) {
|
||||
return c.RunCommandWithOutput("git show --no-patch --pretty=format:%%s %s", sha)
|
||||
}
|
||||
|
||||
// AmendHead amends HEAD with whatever is staged in your working tree
|
||||
func (c *GitCommand) AmendHead() error {
|
||||
return c.OSCommand.RunCommand(c.AmendHeadCmdStr())
|
||||
@@ -69,6 +73,10 @@ func (c *GitCommand) Revert(sha string) error {
|
||||
return c.RunCommand("git revert %s", sha)
|
||||
}
|
||||
|
||||
func (c *GitCommand) RevertMerge(sha string, parentNumber int) error {
|
||||
return c.RunCommand("git revert %s -m %d", sha, parentNumber)
|
||||
}
|
||||
|
||||
// CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD
|
||||
func (c *GitCommand) CherryPickCommits(commits []*models.Commit) error {
|
||||
todo := ""
|
||||
|
||||
@@ -38,6 +38,8 @@ func TestGitCommandResetToCommit(t *testing.T) {
|
||||
|
||||
// TestGitCommandCommitStr is a function.
|
||||
func TestGitCommandCommitStr(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
type scenario struct {
|
||||
testName string
|
||||
message string
|
||||
@@ -50,25 +52,24 @@ func TestGitCommandCommitStr(t *testing.T) {
|
||||
testName: "Commit",
|
||||
message: "test",
|
||||
flags: "",
|
||||
expected: "git commit -m \"test\"",
|
||||
expected: "git commit -m " + gitCmd.OSCommand.Quote("test"),
|
||||
},
|
||||
{
|
||||
testName: "Commit with --no-verify flag",
|
||||
message: "test",
|
||||
flags: "--no-verify",
|
||||
expected: "git commit --no-verify -m \"test\"",
|
||||
expected: "git commit --no-verify -m " + gitCmd.OSCommand.Quote("test"),
|
||||
},
|
||||
{
|
||||
testName: "Commit with multiline message",
|
||||
message: "line1\nline2",
|
||||
flags: "",
|
||||
expected: "git commit -m \"line1\" -m \"line2\"",
|
||||
expected: "git commit -m " + gitCmd.OSCommand.Quote("line1") + " -m " + gitCmd.OSCommand.Quote("line2"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
cmdStr := gitCmd.CommitCmdStr(s.message, s.flags)
|
||||
assert.Equal(t, s.expected, cmdStr)
|
||||
})
|
||||
|
||||
+3
-15
@@ -15,12 +15,8 @@ func (c *GitCommand) ConfiguredPager() string {
|
||||
if os.Getenv("PAGER") != "" {
|
||||
return os.Getenv("PAGER")
|
||||
}
|
||||
output, err := c.RunCommandWithOutput("git config --get-all core.pager")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
trimmedOutput := strings.TrimSpace(output)
|
||||
return strings.Split(trimmedOutput, "\n")[0]
|
||||
output := c.GitConfig.Get("core.pager")
|
||||
return strings.Split(output, "\n")[0]
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetPager(width int) string {
|
||||
@@ -42,11 +38,6 @@ func (c *GitCommand) colorArg() string {
|
||||
return c.Config.GetUserConfig().Git.Paging.ColorArg
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetConfigValue(key string) string {
|
||||
output, _ := c.getGitConfigValue(key)
|
||||
return output
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -55,8 +46,5 @@ func (c *GitCommand) UsingGpg() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
gpgsign := c.GetConfigValue("commit.gpgsign")
|
||||
value := strings.ToLower(gpgsign)
|
||||
|
||||
return value == "true" || value == "1" || value == "yes" || value == "on"
|
||||
return c.GitConfig.GetBool("commit.gpgsign")
|
||||
}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandUsingGpg is a function.
|
||||
func TestGitCommandUsingGpg(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
getGitConfigValue func(string) (string, error)
|
||||
test func(bool)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Option global and local config commit.gpgsign is not set",
|
||||
func(string) (string, error) { return "", nil },
|
||||
func(gpgEnabled bool) {
|
||||
assert.False(t, gpgEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Option commit.gpgsign is true",
|
||||
func(string) (string, error) {
|
||||
return "True", nil
|
||||
},
|
||||
func(gpgEnabled bool) {
|
||||
assert.True(t, gpgEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Option commit.gpgsign is on",
|
||||
func(string) (string, error) {
|
||||
return "ON", nil
|
||||
},
|
||||
func(gpgEnabled bool) {
|
||||
assert.True(t, gpgEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Option commit.gpgsign is yes",
|
||||
func(string) (string, error) {
|
||||
return "YeS", nil
|
||||
},
|
||||
func(gpgEnabled bool) {
|
||||
assert.True(t, gpgEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Option commit.gpgsign is 1",
|
||||
func(string) (string, error) {
|
||||
return "1", nil
|
||||
},
|
||||
func(gpgEnabled bool) {
|
||||
assert.True(t, gpgEnabled)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.getGitConfigValue = s.getGitConfigValue
|
||||
s.test(gitCmd.UsingGpg())
|
||||
})
|
||||
}
|
||||
}
|
||||
+11
-5
@@ -1,6 +1,10 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
@@ -14,11 +18,13 @@ func NewDummyGitCommand() *GitCommand {
|
||||
|
||||
// NewDummyGitCommandWithOSCommand creates a new dummy GitCommand for testing
|
||||
func NewDummyGitCommandWithOSCommand(osCommand *oscommands.OSCommand) *GitCommand {
|
||||
newAppConfig := config.NewDummyAppConfig()
|
||||
return &GitCommand{
|
||||
Log: utils.NewDummyLog(),
|
||||
OSCommand: osCommand,
|
||||
Tr: i18n.NewTranslationSet(utils.NewDummyLog()),
|
||||
Config: config.NewDummyAppConfig(),
|
||||
getGitConfigValue: func(string) (string, error) { return "", nil },
|
||||
Log: utils.NewDummyLog(),
|
||||
OSCommand: osCommand,
|
||||
Tr: i18n.NewTranslationSet(utils.NewDummyLog(), newAppConfig.GetUserConfig().Gui.Language),
|
||||
Config: newAppConfig,
|
||||
GitConfig: git_config.NewFakeGitConfig(map[string]string{}),
|
||||
GetCmdWriter: func() io.Writer { return ioutil.Discard },
|
||||
}
|
||||
}
|
||||
|
||||
+38
-17
@@ -2,8 +2,10 @@ package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
@@ -14,7 +16,11 @@ import (
|
||||
|
||||
// CatFile obtains the content of a file
|
||||
func (c *GitCommand) CatFile(fileName string) (string, error) {
|
||||
return c.OSCommand.CatFile(fileName)
|
||||
buf, err := ioutil.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
|
||||
func (c *GitCommand) OpenMergeToolCmd() string {
|
||||
@@ -117,14 +123,14 @@ func (c *GitCommand) DiscardAllFileChanges(file *models.File) error {
|
||||
if err := c.RunCommand("git checkout --ours -- %s", quotedFileName); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.RunCommand("git add %s", quotedFileName); err != nil {
|
||||
if err := c.RunCommand("git add -- %s", quotedFileName); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if file.ShortStatus == "DU" {
|
||||
return c.RunCommand("git rm %s", quotedFileName)
|
||||
return c.RunCommand("git rm -- %s", quotedFileName)
|
||||
}
|
||||
|
||||
// if the file isn't tracked, we assume you want to delete it
|
||||
@@ -189,17 +195,18 @@ func (c *GitCommand) Ignore(filename string) error {
|
||||
}
|
||||
|
||||
// WorktreeFileDiff returns the diff of a file
|
||||
func (c *GitCommand) WorktreeFileDiff(file *models.File, plain bool, cached bool) string {
|
||||
func (c *GitCommand) WorktreeFileDiff(file *models.File, plain bool, cached bool, ignoreWhitespace bool) string {
|
||||
// for now we assume an error means the file was deleted
|
||||
s, _ := c.OSCommand.RunCommandWithOutput(c.WorktreeFileDiffCmdStr(file, plain, cached))
|
||||
s, _ := c.OSCommand.RunCommandWithOutput(c.WorktreeFileDiffCmdStr(file, plain, cached, ignoreWhitespace))
|
||||
return s
|
||||
}
|
||||
|
||||
func (c *GitCommand) WorktreeFileDiffCmdStr(node models.IFile, plain bool, cached bool) string {
|
||||
func (c *GitCommand) WorktreeFileDiffCmdStr(node models.IFile, plain bool, cached bool, ignoreWhitespace bool) string {
|
||||
cachedArg := ""
|
||||
trackedArg := "--"
|
||||
colorArg := c.colorArg()
|
||||
path := c.OSCommand.Quote(node.GetPath())
|
||||
quotedPath := c.OSCommand.Quote(node.GetPath())
|
||||
ignoreWhitespaceArg := ""
|
||||
if cached {
|
||||
cachedArg = "--cached"
|
||||
}
|
||||
@@ -209,12 +216,15 @@ func (c *GitCommand) WorktreeFileDiffCmdStr(node models.IFile, plain bool, cache
|
||||
if plain {
|
||||
colorArg = "never"
|
||||
}
|
||||
if ignoreWhitespace {
|
||||
ignoreWhitespaceArg = "--ignore-all-space"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("git diff --submodule --no-ext-diff --color=%s %s %s %s", colorArg, cachedArg, trackedArg, path)
|
||||
return fmt.Sprintf("git diff --submodule --no-ext-diff --color=%s %s %s %s %s", colorArg, ignoreWhitespaceArg, cachedArg, trackedArg, quotedPath)
|
||||
}
|
||||
|
||||
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")
|
||||
filepath := filepath.Join(c.Config.GetTempDir(), 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
|
||||
@@ -246,12 +256,12 @@ func (c *GitCommand) ShowFileDiffCmdStr(from string, to string, reverse bool, fi
|
||||
reverseFlag = " -R "
|
||||
}
|
||||
|
||||
return fmt.Sprintf("git diff --submodule --no-ext-diff --no-renames --color=%s %s %s %s -- %s", colorArg, from, to, reverseFlag, fileName)
|
||||
return fmt.Sprintf("git diff --submodule --no-ext-diff --no-renames --color=%s %s %s %s -- %s", colorArg, from, to, reverseFlag, c.OSCommand.Quote(fileName))
|
||||
}
|
||||
|
||||
// CheckoutFile checks out the file for the given commit
|
||||
func (c *GitCommand) CheckoutFile(commitSha, fileName string) error {
|
||||
return c.RunCommand("git checkout %s %s", commitSha, fileName)
|
||||
return c.RunCommand("git checkout %s -- %s", commitSha, c.OSCommand.Quote(fileName))
|
||||
}
|
||||
|
||||
// DiscardOldFileChanges discards changes to a file from an old commit
|
||||
@@ -261,7 +271,7 @@ func (c *GitCommand) DiscardOldFileChanges(commits []*models.Commit, commitIndex
|
||||
}
|
||||
|
||||
// check if file exists in previous commit (this command returns an error if the file doesn't exist)
|
||||
if err := c.RunCommand("git cat-file -e HEAD^:%s", fileName); err != nil {
|
||||
if err := c.RunCommand("git cat-file -e HEAD^:%s", c.OSCommand.Quote(fileName)); err != nil {
|
||||
if err := c.OSCommand.Remove(fileName); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -289,7 +299,7 @@ func (c *GitCommand) DiscardAnyUnstagedFileChanges() error {
|
||||
|
||||
// RemoveTrackedFiles will delete the given file(s) even if they are currently tracked
|
||||
func (c *GitCommand) RemoveTrackedFiles(name string) error {
|
||||
return c.RunCommand("git rm -r --cached %s", name)
|
||||
return c.RunCommand("git rm -r --cached -- %s", c.OSCommand.Quote(name))
|
||||
}
|
||||
|
||||
// RemoveUntrackedFiles runs `git clean -fd`
|
||||
@@ -317,8 +327,12 @@ func (c *GitCommand) ResetAndClean() error {
|
||||
return c.RemoveUntrackedFiles()
|
||||
}
|
||||
|
||||
func (c *GitCommand) EditFileCmdStr(filename string) (string, error) {
|
||||
editor := c.GetConfigValue("core.editor")
|
||||
func (c *GitCommand) EditFileCmdStr(filename string, lineNumber int) (string, error) {
|
||||
editor := c.Config.GetUserConfig().OS.EditCommand
|
||||
|
||||
if editor == "" {
|
||||
editor = c.GitConfig.Get("core.editor")
|
||||
}
|
||||
|
||||
if editor == "" {
|
||||
editor = c.OSCommand.Getenv("GIT_EDITOR")
|
||||
@@ -335,8 +349,15 @@ func (c *GitCommand) EditFileCmdStr(filename string) (string, error) {
|
||||
}
|
||||
}
|
||||
if editor == "" {
|
||||
return "", errors.New("No editor defined in $GIT_EDITOR, $VISUAL, $EDITOR, or git config")
|
||||
return "", errors.New("No editor defined in config file, $GIT_EDITOR, $VISUAL, $EDITOR, or git config")
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %s", editor, c.OSCommand.Quote(filename)), nil
|
||||
templateValues := map[string]string{
|
||||
"editor": editor,
|
||||
"filename": c.OSCommand.Quote(filename),
|
||||
"line": strconv.Itoa(lineNumber),
|
||||
}
|
||||
|
||||
editCmdTemplate := c.Config.GetUserConfig().OS.EditCommandTemplate
|
||||
return utils.ResolvePlaceholderString(editCmdTemplate, templateValues), nil
|
||||
}
|
||||
|
||||
+113
-81
@@ -4,37 +4,15 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandCatFile tests emitting a file using commands, where commands vary by OS.
|
||||
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)
|
||||
assert.EqualValues(t, []string{"test.txt"}, args)
|
||||
|
||||
return secureexec.Command("echo", "-n", "test")
|
||||
}
|
||||
|
||||
o, err := gitCmd.CatFile("test.txt")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test", o)
|
||||
}
|
||||
|
||||
// TestGitCommandStageFile is a function.
|
||||
func TestGitCommandStageFile(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
@@ -333,11 +311,12 @@ func TestGitCommandDiscardAllFileChanges(t *testing.T) {
|
||||
// TestGitCommandDiff is a function.
|
||||
func TestGitCommandDiff(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
file *models.File
|
||||
plain bool
|
||||
cached bool
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
file *models.File
|
||||
plain bool
|
||||
cached bool
|
||||
ignoreWhitespace bool
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
@@ -356,6 +335,7 @@ func TestGitCommandDiff(t *testing.T) {
|
||||
},
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"cached",
|
||||
@@ -372,6 +352,7 @@ func TestGitCommandDiff(t *testing.T) {
|
||||
},
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"plain",
|
||||
@@ -388,6 +369,7 @@ func TestGitCommandDiff(t *testing.T) {
|
||||
},
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"File not tracked and file has no staged changes",
|
||||
@@ -404,6 +386,24 @@ func TestGitCommandDiff(t *testing.T) {
|
||||
},
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Default case (ignore whitespace)",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=always", "--ignore-all-space", "--", "test.txt"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
&models.File{
|
||||
Name: "test.txt",
|
||||
HasStagedChanges: false,
|
||||
Tracked: true,
|
||||
},
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -411,7 +411,7 @@ func TestGitCommandDiff(t *testing.T) {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
gitCmd.WorktreeFileDiff(s.file, s.plain, s.cached)
|
||||
gitCmd.WorktreeFileDiff(s.file, s.plain, s.cached, s.ignoreWhitespace)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -433,7 +433,7 @@ func TestGitCommandCheckoutFile(t *testing.T) {
|
||||
"test999.txt",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git checkout 11af912 test999.txt",
|
||||
Expect: "git checkout 11af912 -- test999.txt",
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
@@ -447,7 +447,7 @@ func TestGitCommandCheckoutFile(t *testing.T) {
|
||||
"test999.txt",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git checkout 11af912 test999.txt",
|
||||
Expect: "git checkout 11af912 -- test999.txt",
|
||||
Replace: "test",
|
||||
},
|
||||
}),
|
||||
@@ -524,24 +524,21 @@ func TestGitCommandApplyPatch(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandDiscardOldFileChanges is a function.
|
||||
func TestGitCommandDiscardOldFileChanges(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
getGitConfigValue func(string) (string, error)
|
||||
commits []*models.Commit
|
||||
commitIndex int
|
||||
fileName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
testName string
|
||||
gitConfigMockResponses map[string]string
|
||||
commits []*models.Commit
|
||||
commitIndex int
|
||||
fileName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"returns error when index outside of range of commits",
|
||||
func(string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
nil,
|
||||
[]*models.Commit{},
|
||||
0,
|
||||
"test999.txt",
|
||||
@@ -552,9 +549,7 @@ func TestGitCommandDiscardOldFileChanges(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"returns error when using gpg",
|
||||
func(string) (string, error) {
|
||||
return "true", nil
|
||||
},
|
||||
map[string]string{"commit.gpgsign": "true"},
|
||||
[]*models.Commit{{Name: "commit", Sha: "123456"}},
|
||||
0,
|
||||
"test999.txt",
|
||||
@@ -565,9 +560,7 @@ func TestGitCommandDiscardOldFileChanges(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"checks out file if it already existed",
|
||||
func(string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
nil,
|
||||
[]*models.Commit{
|
||||
{Name: "commit", Sha: "123456"},
|
||||
{Name: "commit2", Sha: "abcdef"},
|
||||
@@ -584,7 +577,7 @@ func TestGitCommandDiscardOldFileChanges(t *testing.T) {
|
||||
Replace: "echo",
|
||||
},
|
||||
{
|
||||
Expect: "git checkout HEAD^ test999.txt",
|
||||
Expect: "git checkout HEAD^ -- test999.txt",
|
||||
Replace: "echo",
|
||||
},
|
||||
{
|
||||
@@ -609,7 +602,7 @@ func TestGitCommandDiscardOldFileChanges(t *testing.T) {
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
gitCmd.getGitConfigValue = s.getGitConfigValue
|
||||
gitCmd.GitConfig = git_config.NewFakeGitConfig(s.gitConfigMockResponses)
|
||||
s.test(gitCmd.DiscardOldFileChanges(s.commits, s.commitIndex, s.fileName))
|
||||
})
|
||||
}
|
||||
@@ -718,32 +711,55 @@ func TestGitCommandRemoveUntrackedFiles(t *testing.T) {
|
||||
|
||||
// TestEditFileCmdStr is a function.
|
||||
func TestEditFileCmdStr(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
type scenario struct {
|
||||
filename string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
getenv func(string) string
|
||||
getGitConfigValue func(string) (string, error)
|
||||
test func(string, error)
|
||||
filename string
|
||||
configEditCommand string
|
||||
configEditCommandTemplate string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
getenv func(string) string
|
||||
gitConfigMockResponses map[string]string
|
||||
test func(string, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"test",
|
||||
"",
|
||||
"{{editor}} {{filename}}",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
return secureexec.Command("exit", "1")
|
||||
},
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
nil,
|
||||
func(cmdStr string, err error) {
|
||||
assert.EqualError(t, err, "No editor defined in $GIT_EDITOR, $VISUAL, $EDITOR, or git config")
|
||||
assert.EqualError(t, err, "No editor defined in config file, $GIT_EDITOR, $VISUAL, $EDITOR, or git config")
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
"nano",
|
||||
"{{editor}} {{filename}}",
|
||||
func(name string, args ...string) *exec.Cmd {
|
||||
assert.Equal(t, "which", name)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
nil,
|
||||
func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "nano "+gitCmd.OSCommand.Quote("test"), cmdStr)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
"",
|
||||
"{{editor}} {{filename}}",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "which", name)
|
||||
return secureexec.Command("exit", "1")
|
||||
@@ -751,16 +767,16 @@ func TestEditFileCmdStr(t *testing.T) {
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "nano", nil
|
||||
},
|
||||
map[string]string{"core.editor": "nano"},
|
||||
func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "nano \"test\"", cmdStr)
|
||||
assert.Equal(t, "nano "+gitCmd.OSCommand.Quote("test"), cmdStr)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
"",
|
||||
"{{editor}} {{filename}}",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "which", name)
|
||||
return secureexec.Command("exit", "1")
|
||||
@@ -772,15 +788,15 @@ func TestEditFileCmdStr(t *testing.T) {
|
||||
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
nil,
|
||||
func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
"",
|
||||
"{{editor}} {{filename}}",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "which", name)
|
||||
return secureexec.Command("exit", "1")
|
||||
@@ -792,16 +808,16 @@ func TestEditFileCmdStr(t *testing.T) {
|
||||
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
nil,
|
||||
func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "emacs \"test\"", cmdStr)
|
||||
assert.Equal(t, "emacs "+gitCmd.OSCommand.Quote("test"), cmdStr)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
"",
|
||||
"{{editor}} {{filename}}",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "which", name)
|
||||
return secureexec.Command("echo")
|
||||
@@ -809,16 +825,16 @@ func TestEditFileCmdStr(t *testing.T) {
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
nil,
|
||||
func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "vi \"test\"", cmdStr)
|
||||
assert.Equal(t, "vi "+gitCmd.OSCommand.Quote("test"), cmdStr)
|
||||
},
|
||||
},
|
||||
{
|
||||
"file/with space",
|
||||
"",
|
||||
"{{editor}} {{filename}}",
|
||||
func(name string, args ...string) *exec.Cmd {
|
||||
assert.Equal(t, "which", name)
|
||||
return secureexec.Command("echo")
|
||||
@@ -826,21 +842,37 @@ func TestEditFileCmdStr(t *testing.T) {
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
nil,
|
||||
func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "vi \"file/with space\"", cmdStr)
|
||||
assert.Equal(t, "vi "+gitCmd.OSCommand.Quote("file/with space"), cmdStr)
|
||||
},
|
||||
},
|
||||
{
|
||||
"open file/at line",
|
||||
"vim",
|
||||
"{{editor}} +{{line}} {{filename}}",
|
||||
func(name string, args ...string) *exec.Cmd {
|
||||
assert.Equal(t, "which", name)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
nil,
|
||||
func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "vim +1 "+gitCmd.OSCommand.Quote("open file/at line"), cmdStr)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.Config.GetUserConfig().OS.EditCommand = s.configEditCommand
|
||||
gitCmd.Config.GetUserConfig().OS.EditCommandTemplate = s.configEditCommandTemplate
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
gitCmd.OSCommand.Getenv = s.getenv
|
||||
gitCmd.getGitConfigValue = s.getGitConfigValue
|
||||
s.test(gitCmd.EditFileCmdStr(s.filename))
|
||||
gitCmd.GitConfig = git_config.NewFakeGitConfig(s.gitConfigMockResponses)
|
||||
s.test(gitCmd.EditFileCmdStr(s.filename, 1))
|
||||
}
|
||||
}
|
||||
|
||||
+30
-18
@@ -1,6 +1,7 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
gogit "github.com/jesseduffield/go-git/v5"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
@@ -32,32 +34,37 @@ type GitCommand struct {
|
||||
Repo *gogit.Repository
|
||||
Tr *i18n.TranslationSet
|
||||
Config config.AppConfigurer
|
||||
getGitConfigValue func(string) (string, error)
|
||||
DotGitDir string
|
||||
onSuccessfulContinue func() error
|
||||
PatchManager *patch.PatchManager
|
||||
GitConfig git_config.IGitConfig
|
||||
|
||||
// Push to current determines whether the user has configured to push to the remote branch of the same name as the current or not
|
||||
PushToCurrent bool
|
||||
|
||||
// this is just a view that we write to when running certain commands.
|
||||
// Coincidentally at the moment it's the same view that OnRunCommand logs to
|
||||
// but that need not always be the case.
|
||||
GetCmdWriter func() io.Writer
|
||||
}
|
||||
|
||||
// NewGitCommand it runs git commands
|
||||
func NewGitCommand(log *logrus.Entry, osCommand *oscommands.OSCommand, tr *i18n.TranslationSet, config config.AppConfigurer) (*GitCommand, error) {
|
||||
func NewGitCommand(
|
||||
log *logrus.Entry,
|
||||
osCommand *oscommands.OSCommand,
|
||||
tr *i18n.TranslationSet,
|
||||
config config.AppConfigurer,
|
||||
gitConfig git_config.IGitConfig,
|
||||
) (*GitCommand, error) {
|
||||
var repo *gogit.Repository
|
||||
|
||||
// see what our default push behaviour is
|
||||
output, err := osCommand.RunCommandWithOutput("git config --get push.default")
|
||||
pushToCurrent := false
|
||||
if err != nil {
|
||||
log.Errorf("error reading git config: %v", err)
|
||||
} else {
|
||||
pushToCurrent = strings.TrimSpace(output) == "current"
|
||||
}
|
||||
pushToCurrent := gitConfig.Get("push.default") == "current"
|
||||
|
||||
if err := navigateToRepoRootDirectory(os.Stat, os.Chdir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var err error
|
||||
if repo, err = setupRepository(gogit.PlainOpen, tr.GitconfigParseErr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -68,14 +75,15 @@ func NewGitCommand(log *logrus.Entry, osCommand *oscommands.OSCommand, tr *i18n.
|
||||
}
|
||||
|
||||
gitCommand := &GitCommand{
|
||||
Log: log,
|
||||
OSCommand: osCommand,
|
||||
Tr: tr,
|
||||
Repo: repo,
|
||||
Config: config,
|
||||
getGitConfigValue: getGitConfigValue,
|
||||
DotGitDir: dotGitDir,
|
||||
PushToCurrent: pushToCurrent,
|
||||
Log: log,
|
||||
OSCommand: osCommand,
|
||||
Tr: tr,
|
||||
Repo: repo,
|
||||
Config: config,
|
||||
DotGitDir: dotGitDir,
|
||||
PushToCurrent: pushToCurrent,
|
||||
GitConfig: gitConfig,
|
||||
GetCmdWriter: func() io.Writer { return ioutil.Discard },
|
||||
}
|
||||
|
||||
gitCommand.PatchManager = patch.NewPatchManager(log, gitCommand.ApplyPatch, gitCommand.ShowFileDiff)
|
||||
@@ -246,3 +254,7 @@ func (c *GitCommand) RunCommandWithOutput(formatString string, formatArgs ...int
|
||||
return output, err
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GitCommand) NewCmdObjFromStr(cmdStr string) oscommands.ICmdObj {
|
||||
return c.OSCommand.NewCmdObjFromStr(cmdStr).AddEnvVars("GIT_OPTIONAL_LOCKS=0")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package git_config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type IGitConfig interface {
|
||||
Get(string) string
|
||||
GetBool(string) bool
|
||||
}
|
||||
|
||||
type CachedGitConfig struct {
|
||||
cache map[string]string
|
||||
getKey func(string) (string, error)
|
||||
log *logrus.Entry
|
||||
}
|
||||
|
||||
func NewStdCachedGitConfig(log *logrus.Entry) *CachedGitConfig {
|
||||
return NewCachedGitConfig(getGitConfigValue, log)
|
||||
}
|
||||
|
||||
func NewCachedGitConfig(getKey func(string) (string, error), log *logrus.Entry) *CachedGitConfig {
|
||||
return &CachedGitConfig{
|
||||
cache: make(map[string]string),
|
||||
getKey: getKey,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *CachedGitConfig) Get(key string) string {
|
||||
if value, ok := self.cache[key]; ok {
|
||||
self.log.Debugf("using cache for key " + key)
|
||||
return value
|
||||
}
|
||||
|
||||
value := self.getAux(key)
|
||||
self.cache[key] = value
|
||||
return value
|
||||
}
|
||||
|
||||
func (self *CachedGitConfig) getAux(key string) string {
|
||||
value, err := self.getKey(key)
|
||||
if err != nil {
|
||||
self.log.Debugf("Error getting git config value for key: " + key + ". Error: " + err.Error())
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
|
||||
func (self *CachedGitConfig) GetBool(key string) bool {
|
||||
return isTruthy(self.Get(key))
|
||||
}
|
||||
|
||||
func isTruthy(value string) bool {
|
||||
lcValue := strings.ToLower(value)
|
||||
return lcValue == "true" || lcValue == "1" || lcValue == "yes" || lcValue == "on"
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package git_config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetBool(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
mockResponses map[string]string
|
||||
expected bool
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Option global and local config commit.gpgsign is not set",
|
||||
map[string]string{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Some other random key is set",
|
||||
map[string]string{"blah": "blah"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Option commit.gpgsign is true",
|
||||
map[string]string{"commit.gpgsign": "True"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Option commit.gpgsign is on",
|
||||
map[string]string{"commit.gpgsign": "ON"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Option commit.gpgsign is yes",
|
||||
map[string]string{"commit.gpgsign": "YeS"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Option commit.gpgsign is 1",
|
||||
map[string]string{"commit.gpgsign": "1"},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
fake := NewFakeGitConfig(s.mockResponses)
|
||||
real := NewCachedGitConfig(
|
||||
func(key string) (string, error) {
|
||||
return fake.Get(key), nil
|
||||
},
|
||||
utils.NewDummyLog(),
|
||||
)
|
||||
result := real.GetBool("commit.gpgsign")
|
||||
assert.Equal(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
mockResponses map[string]string
|
||||
expected string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"not set",
|
||||
map[string]string{},
|
||||
"",
|
||||
},
|
||||
{
|
||||
"is set",
|
||||
map[string]string{"commit.gpgsign": "blah"},
|
||||
"blah",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
fake := NewFakeGitConfig(s.mockResponses)
|
||||
real := NewCachedGitConfig(
|
||||
func(key string) (string, error) {
|
||||
return fake.Get(key), nil
|
||||
},
|
||||
utils.NewDummyLog(),
|
||||
)
|
||||
result := real.Get("commit.gpgsign")
|
||||
assert.Equal(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// verifying that the cache is used
|
||||
count := 0
|
||||
real := NewCachedGitConfig(
|
||||
func(key string) (string, error) {
|
||||
count++
|
||||
assert.Equal(t, "commit.gpgsign", key)
|
||||
return "blah", nil
|
||||
},
|
||||
utils.NewDummyLog(),
|
||||
)
|
||||
result := real.Get("commit.gpgsign")
|
||||
assert.Equal(t, "blah", result)
|
||||
result = real.Get("commit.gpgsign")
|
||||
assert.Equal(t, "blah", result)
|
||||
assert.Equal(t, 1, count)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package git_config
|
||||
|
||||
type FakeGitConfig struct {
|
||||
mockResponses map[string]string
|
||||
}
|
||||
|
||||
func NewFakeGitConfig(mockResponses map[string]string) *FakeGitConfig {
|
||||
return &FakeGitConfig{
|
||||
mockResponses: mockResponses,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *FakeGitConfig) Get(key string) string {
|
||||
if self.mockResponses == nil {
|
||||
return ""
|
||||
}
|
||||
return self.mockResponses[key]
|
||||
}
|
||||
|
||||
func (self *FakeGitConfig) GetBool(key string) bool {
|
||||
return isTruthy(self.Get(key))
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package commands
|
||||
package git_config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
gogit "github.com/jesseduffield/go-git/v5"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
@@ -209,7 +210,8 @@ 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()))
|
||||
newAppConfig := config.NewDummyAppConfig()
|
||||
s.test(NewGitCommand(utils.NewDummyLog(), oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog(), newAppConfig.GetUserConfig().Gui.Language), newAppConfig, git_config.NewFakeGitConfig(nil)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,12 @@ func (b *BranchListBuilder) obtainBranches() []*models.Branch {
|
||||
}
|
||||
|
||||
split := strings.Split(line, SEPARATION_CHAR)
|
||||
if len(split) != 4 {
|
||||
// Ignore line if it isn't separated into 4 parts
|
||||
// This is probably a warning message, for more info see:
|
||||
// https://github.com/jesseduffield/lazygit/issues/1385#issuecomment-885580439
|
||||
continue
|
||||
}
|
||||
|
||||
name := strings.TrimPrefix(split[1], "heads/")
|
||||
branch := &models.Branch{
|
||||
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
"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/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -37,7 +37,12 @@ type CommitListBuilder struct {
|
||||
}
|
||||
|
||||
// NewCommitListBuilder builds a new commit list builder
|
||||
func NewCommitListBuilder(log *logrus.Entry, gitCommand *GitCommand, osCommand *oscommands.OSCommand, tr *i18n.TranslationSet) *CommitListBuilder {
|
||||
func NewCommitListBuilder(
|
||||
log *logrus.Entry,
|
||||
gitCommand *GitCommand,
|
||||
osCommand *oscommands.OSCommand,
|
||||
tr *i18n.TranslationSet,
|
||||
) *CommitListBuilder {
|
||||
return &CommitListBuilder{
|
||||
Log: log,
|
||||
GitCommand: gitCommand,
|
||||
@@ -72,10 +77,6 @@ func (c *CommitListBuilder) extractCommitFromLine(line string) *models.Commit {
|
||||
|
||||
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,
|
||||
@@ -83,7 +84,7 @@ func (c *CommitListBuilder) extractCommitFromLine(line string) *models.Commit {
|
||||
ExtraInfo: extraInfo,
|
||||
UnixTimestamp: int64(unitTimestampInt),
|
||||
Author: author,
|
||||
IsMerge: isMerge,
|
||||
Parents: strings.Split(parentHashes, " "),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +93,8 @@ type GetCommitsOptions struct {
|
||||
FilterPath string
|
||||
IncludeRebaseCommits bool
|
||||
RefName string // e.g. "HEAD" or "my_branch"
|
||||
// determines if we show the whole git graph i.e. pass the '--all' flag
|
||||
All bool
|
||||
}
|
||||
|
||||
func (c *CommitListBuilder) MergeRebasingCommits(commits []*models.Commit) ([]*models.Commit, error) {
|
||||
@@ -114,7 +117,7 @@ func (c *CommitListBuilder) MergeRebasingCommits(commits []*models.Commit) ([]*m
|
||||
return result, nil
|
||||
}
|
||||
|
||||
rebasingCommits, err := c.getRebasingCommits(rebaseMode)
|
||||
rebasingCommits, err := c.getHydratedRebasingCommits(rebaseMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -153,7 +156,7 @@ func (c *CommitListBuilder) GetCommits(opts GetCommitsOptions) ([]*models.Commit
|
||||
cmd := c.getLogCmd(opts)
|
||||
|
||||
err = oscommands.RunLineOutputCmd(cmd, func(line string) (bool, error) {
|
||||
if strings.Split(line, " ")[0] != "gpg:" {
|
||||
if canExtractCommit(line) {
|
||||
commit := c.extractCommitFromLine(line)
|
||||
if commit.Sha == firstPushedCommit {
|
||||
passedFirstPushedCommit = true
|
||||
@@ -169,8 +172,7 @@ func (c *CommitListBuilder) GetCommits(opts GetCommitsOptions) ([]*models.Commit
|
||||
|
||||
if rebaseMode != "" {
|
||||
currentCommit := commits[len(rebasingCommits)]
|
||||
blue := color.New(color.FgYellow)
|
||||
youAreHere := blue.Sprintf("<-- %s ---", c.Tr.YouAreHere)
|
||||
youAreHere := style.FgYellow.Sprintf("<-- %s ---", c.Tr.YouAreHere)
|
||||
currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name)
|
||||
}
|
||||
|
||||
@@ -182,6 +184,51 @@ func (c *CommitListBuilder) GetCommits(opts GetCommitsOptions) ([]*models.Commit
|
||||
return commits, nil
|
||||
}
|
||||
|
||||
func (c *CommitListBuilder) getHydratedRebasingCommits(rebaseMode string) ([]*models.Commit, error) {
|
||||
commits, err := c.getRebasingCommits(rebaseMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(commits) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
commitShas := make([]string, len(commits))
|
||||
for i, commit := range commits {
|
||||
commitShas[i] = commit.Sha
|
||||
}
|
||||
|
||||
// note that we're not filtering these as we do non-rebasing commits just because
|
||||
// I suspect that will cause some damage
|
||||
cmd := c.OSCommand.ExecutableFromString(
|
||||
fmt.Sprintf(
|
||||
"git show %s --no-patch --oneline %s --abbrev=%d",
|
||||
strings.Join(commitShas, " "),
|
||||
prettyFormat,
|
||||
20,
|
||||
),
|
||||
)
|
||||
|
||||
hydratedCommits := make([]*models.Commit, 0, len(commits))
|
||||
i := 0
|
||||
err = oscommands.RunLineOutputCmd(cmd, func(line string) (bool, error) {
|
||||
if canExtractCommit(line) {
|
||||
commit := c.extractCommitFromLine(line)
|
||||
matchingCommit := commits[i]
|
||||
commit.Action = matchingCommit.Action
|
||||
commit.Status = matchingCommit.Status
|
||||
hydratedCommits = append(hydratedCommits, commit)
|
||||
i++
|
||||
}
|
||||
return false, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hydratedCommits, nil
|
||||
}
|
||||
|
||||
// getRebasingCommits obtains the commits that we're in the process of rebasing
|
||||
func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*models.Commit, error) {
|
||||
switch rebaseMode {
|
||||
@@ -327,7 +374,7 @@ func (c *CommitListBuilder) getMergeBase(refName string) (string, error) {
|
||||
}
|
||||
|
||||
// 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)
|
||||
output, _ := c.OSCommand.RunCommandWithOutput("git merge-base %s %s", c.OSCommand.Quote(refName), c.OSCommand.Quote(baseBranch))
|
||||
return ignoringWarnings(output), nil
|
||||
}
|
||||
|
||||
@@ -344,7 +391,7 @@ func ignoringWarnings(commandOutput string) string {
|
||||
// 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)
|
||||
output, err := c.OSCommand.RunCommandWithOutput("git merge-base %s %s@{u}", c.OSCommand.Quote(refName), c.OSCommand.Quote(refName))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -364,18 +411,37 @@ func (c *CommitListBuilder) getLogCmd(opts GetCommitsOptions) *exec.Cmd {
|
||||
filterFlag = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(opts.FilterPath))
|
||||
}
|
||||
|
||||
config := c.GitCommand.Config.GetUserConfig().Git.Log
|
||||
|
||||
orderFlag := "--" + config.Order
|
||||
allFlag := ""
|
||||
if opts.All {
|
||||
allFlag = " --all"
|
||||
}
|
||||
|
||||
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,
|
||||
"git log %s %s %s --oneline %s %s --abbrev=%d %s",
|
||||
c.OSCommand.Quote(opts.RefName),
|
||||
orderFlag,
|
||||
allFlag,
|
||||
prettyFormat,
|
||||
limitFlag,
|
||||
20,
|
||||
filterFlag,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
var prettyFormat = fmt.Sprintf(
|
||||
"--pretty=format:\"%%H%s%%at%s%%aN%s%%d%s%%p%s%%s\"",
|
||||
SEPARATION_CHAR,
|
||||
SEPARATION_CHAR,
|
||||
SEPARATION_CHAR,
|
||||
SEPARATION_CHAR,
|
||||
SEPARATION_CHAR,
|
||||
)
|
||||
|
||||
func canExtractCommit(line string) bool {
|
||||
return strings.Split(line, " ")[0] != "gpg:"
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ func NewDummyCommitListBuilder() *CommitListBuilder {
|
||||
Log: utils.NewDummyLog(),
|
||||
GitCommand: NewDummyGitCommandWithOSCommand(osCommand),
|
||||
OSCommand: osCommand,
|
||||
Tr: i18n.NewTranslationSet(utils.NewDummyLog()),
|
||||
Tr: i18n.NewTranslationSet(utils.NewDummyLog(), "auto"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
const RENAME_SEPARATOR = " -> "
|
||||
|
||||
// GetStatusFiles git status files
|
||||
type GetStatusFileOptions struct {
|
||||
NoRenames bool
|
||||
@@ -17,44 +15,36 @@ type GetStatusFileOptions struct {
|
||||
|
||||
func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
|
||||
// check if config wants us ignoring untracked files
|
||||
untrackedFilesSetting := c.GetConfigValue("status.showUntrackedFiles")
|
||||
untrackedFilesSetting := c.GitConfig.Get("status.showUntrackedFiles")
|
||||
|
||||
if untrackedFilesSetting == "" {
|
||||
untrackedFilesSetting = "all"
|
||||
}
|
||||
untrackedFilesArg := fmt.Sprintf("--untracked-files=%s", untrackedFilesSetting)
|
||||
|
||||
statusOutput, err := c.GitStatus(GitStatusOptions{NoRenames: opts.NoRenames, UntrackedFilesArg: untrackedFilesArg})
|
||||
statuses, 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)
|
||||
for _, status := range statuses {
|
||||
if strings.HasPrefix(status.StatusString, "warning") {
|
||||
c.Log.Warningf("warning when calling git status: %s", status.StatusString)
|
||||
continue
|
||||
}
|
||||
change := statusString[0:2]
|
||||
change := status.Change
|
||||
stagedChange := change[0:1]
|
||||
unstagedChange := statusString[1:2]
|
||||
name := statusString[3:]
|
||||
unstagedChange := change[1:2]
|
||||
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)
|
||||
previousName := ""
|
||||
if strings.Contains(name, RENAME_SEPARATOR) {
|
||||
split := strings.Split(name, RENAME_SEPARATOR)
|
||||
name = split[1]
|
||||
previousName = split[0]
|
||||
}
|
||||
|
||||
file := &models.File{
|
||||
Name: name,
|
||||
PreviousName: previousName,
|
||||
DisplayString: statusString,
|
||||
Name: status.Name,
|
||||
PreviousName: status.PreviousName,
|
||||
DisplayString: status.StatusString,
|
||||
HasStagedChanges: !hasNoStagedChanges,
|
||||
HasUnstagedChanges: unstagedChange != " ",
|
||||
Tracked: !untracked,
|
||||
@@ -62,7 +52,7 @@ func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
|
||||
Added: unstagedChange == "A" || untracked,
|
||||
HasMergeConflicts: hasMergeConflicts,
|
||||
HasInlineMergeConflicts: hasInlineMergeConflicts,
|
||||
Type: c.OSCommand.FileType(name),
|
||||
Type: c.OSCommand.FileType(status.Name),
|
||||
ShortStatus: change,
|
||||
}
|
||||
files = append(files, file)
|
||||
@@ -71,13 +61,20 @@ func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
|
||||
return files
|
||||
}
|
||||
|
||||
// GitStatus returns the plaintext short status of the repo
|
||||
// GitStatus returns the file status of the repo
|
||||
type GitStatusOptions struct {
|
||||
NoRenames bool
|
||||
UntrackedFilesArg string
|
||||
}
|
||||
|
||||
func (c *GitCommand) GitStatus(opts GitStatusOptions) (string, error) {
|
||||
type FileStatus struct {
|
||||
StatusString string
|
||||
Change string // ??, MM, AM, ...
|
||||
Name string
|
||||
PreviousName string
|
||||
}
|
||||
|
||||
func (c *GitCommand) GitStatus(opts GitStatusOptions) ([]FileStatus, error) {
|
||||
noRenamesFlag := ""
|
||||
if opts.NoRenames {
|
||||
noRenamesFlag = "--no-renames"
|
||||
@@ -85,20 +82,35 @@ func (c *GitCommand) GitStatus(opts GitStatusOptions) (string, error) {
|
||||
|
||||
statusLines, err := c.RunCommandWithOutput("git status %s --porcelain -z %s", opts.UntrackedFilesArg, noRenamesFlag)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return []FileStatus{}, err
|
||||
}
|
||||
|
||||
splitLines := strings.Split(statusLines, "\x00")
|
||||
// if a line starts with 'R' then the next line is the original file.
|
||||
for i := 0; i < len(splitLines)-1; i++ {
|
||||
response := []FileStatus{}
|
||||
|
||||
for i := 0; i < len(splitLines); i++ {
|
||||
original := splitLines[i]
|
||||
if strings.HasPrefix(original, "R ") {
|
||||
next := splitLines[i+1]
|
||||
updated := "R " + next + RENAME_SEPARATOR + strings.TrimPrefix(original, "R ")
|
||||
splitLines[i] = updated
|
||||
splitLines = append(splitLines[0:i+1], splitLines[i+2:]...)
|
||||
|
||||
if len(original) < 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
status := FileStatus{
|
||||
StatusString: original,
|
||||
Change: original[:2],
|
||||
Name: original[3:],
|
||||
PreviousName: "",
|
||||
}
|
||||
|
||||
if strings.HasPrefix(status.Change, "R") {
|
||||
// if a line starts with 'R' then the next line is the original file.
|
||||
status.PreviousName = strings.TrimSpace(splitLines[i+1])
|
||||
status.StatusString = fmt.Sprintf("%s %s -> %s", status.Change, status.PreviousName, status.Name)
|
||||
i++
|
||||
}
|
||||
|
||||
response = append(response, status)
|
||||
}
|
||||
|
||||
return strings.Join(splitLines, "\n"), nil
|
||||
return response, nil
|
||||
}
|
||||
|
||||
@@ -31,8 +31,8 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
|
||||
"Several files found",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return secureexec.Command(
|
||||
"echo",
|
||||
"MM file1.txt\nA file3.txt\nAM file2.txt\n?? file4.txt\nUU file5.txt",
|
||||
"printf",
|
||||
`MM file1.txt\0A file3.txt\0AM file2.txt\0?? file4.txt\0UU file5.txt`,
|
||||
)
|
||||
},
|
||||
func(files []*models.File) {
|
||||
@@ -106,6 +106,111 @@ func TestGitCommandGetStatusFiles(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
assert.EqualValues(t, expected, files)
|
||||
},
|
||||
},
|
||||
{
|
||||
"File with new line char",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return secureexec.Command(
|
||||
"printf",
|
||||
`MM a\nb.txt`,
|
||||
)
|
||||
},
|
||||
func(files []*models.File) {
|
||||
assert.Len(t, files, 1)
|
||||
|
||||
expected := []*models.File{
|
||||
{
|
||||
Name: "a\nb.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Added: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "MM a\nb.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "MM",
|
||||
},
|
||||
}
|
||||
|
||||
assert.EqualValues(t, expected, files)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Renamed files",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return secureexec.Command(
|
||||
"printf",
|
||||
`R after1.txt\0before1.txt\0RM after2.txt\0before2.txt`,
|
||||
)
|
||||
},
|
||||
func(files []*models.File) {
|
||||
assert.Len(t, files, 2)
|
||||
|
||||
expected := []*models.File{
|
||||
{
|
||||
Name: "after1.txt",
|
||||
PreviousName: "before1.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: false,
|
||||
Tracked: true,
|
||||
Added: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "R before1.txt -> after1.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "R ",
|
||||
},
|
||||
{
|
||||
Name: "after2.txt",
|
||||
PreviousName: "before2.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Added: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "RM before2.txt -> after2.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "RM",
|
||||
},
|
||||
}
|
||||
|
||||
assert.EqualValues(t, expected, files)
|
||||
},
|
||||
},
|
||||
{
|
||||
"File with arrow in name",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return secureexec.Command(
|
||||
"printf",
|
||||
`?? a -> b.txt`,
|
||||
)
|
||||
},
|
||||
func(files []*models.File) {
|
||||
assert.Len(t, files, 1)
|
||||
|
||||
expected := []*models.File{
|
||||
{
|
||||
Name: "a -> b.txt",
|
||||
HasStagedChanges: false,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "?? a -> b.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "??",
|
||||
},
|
||||
}
|
||||
|
||||
assert.EqualValues(t, expected, files)
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2,8 +2,8 @@ package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
@@ -13,26 +13,25 @@ import (
|
||||
// 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))
|
||||
cmd := c.OSCommand.ExecutableFromString(fmt.Sprintf(`git log -g --abbrev=20 --format="%%h %%ct %%gs" %s`, filterPathArg))
|
||||
onlyObtainedNewReflogCommits := false
|
||||
err := oscommands.RunLineOutputCmd(cmd, func(line string) (bool, error) {
|
||||
match := re.FindStringSubmatch(line)
|
||||
if len(match) <= 1 {
|
||||
fields := strings.SplitN(line, " ", 3)
|
||||
if len(fields) <= 2 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
unixTimestamp, _ := strconv.Atoi(match[2])
|
||||
unixTimestamp, _ := strconv.Atoi(fields[1])
|
||||
|
||||
commit := &models.Commit{
|
||||
Sha: match[1],
|
||||
Name: match[3],
|
||||
Sha: fields[0],
|
||||
Name: fields[2],
|
||||
UnixTimestamp: int64(unixTimestamp),
|
||||
Status: "reflog",
|
||||
}
|
||||
|
||||
@@ -1,28 +1,16 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
const semverRegex = `v?((\d+\.?)+)([^\d]?.*)`
|
||||
|
||||
func convertToInt(s string) int {
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetTags() ([]*models.Tag, error) {
|
||||
// get remote branches
|
||||
remoteBranchesStr, err := c.OSCommand.RunCommandWithOutput(`git tag --list`)
|
||||
// get remote branches, sorted by creation date (descending)
|
||||
// see: https://git-scm.com/docs/git-tag#Documentation/git-tag.txt---sortltkeygt
|
||||
remoteBranchesStr, err := c.OSCommand.RunCommandWithOutput(`git tag --list --sort=-creatordate`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -37,52 +25,10 @@ func (c *GitCommand) GetTags() ([]*models.Tag, error) {
|
||||
// first step is to get our remotes from go-git
|
||||
tags := make([]*models.Tag, len(split))
|
||||
for i, tagName := range split {
|
||||
|
||||
tags[i] = &models.Tag{
|
||||
Name: tagName,
|
||||
}
|
||||
}
|
||||
|
||||
// now lets sort our tags by name numerically
|
||||
re := regexp.MustCompile(semverRegex)
|
||||
|
||||
// the reason this is complicated is because we're both sorting alphabetically
|
||||
// and when we're dealing with semver strings
|
||||
sort.Slice(tags, func(i, j int) bool {
|
||||
a := tags[i].Name
|
||||
b := tags[j].Name
|
||||
|
||||
matchA := re.FindStringSubmatch(a)
|
||||
matchB := re.FindStringSubmatch(b)
|
||||
|
||||
if len(matchA) > 0 && len(matchB) > 0 {
|
||||
numbersA := strings.Split(matchA[1], ".")
|
||||
numbersB := strings.Split(matchB[1], ".")
|
||||
k := 0
|
||||
for {
|
||||
if len(numbersA) == k && len(numbersB) == k {
|
||||
break
|
||||
}
|
||||
if len(numbersA) == k {
|
||||
return true
|
||||
}
|
||||
if len(numbersB) == k {
|
||||
return false
|
||||
}
|
||||
if convertToInt(numbersA[k]) < convertToInt(numbersB[k]) {
|
||||
return true
|
||||
}
|
||||
if convertToInt(numbersA[k]) > convertToInt(numbersB[k]) {
|
||||
return false
|
||||
}
|
||||
k++
|
||||
}
|
||||
|
||||
return strings.ToLower(matchA[3]) < strings.ToLower(matchB[3])
|
||||
}
|
||||
|
||||
return strings.ToLower(a) < strings.ToLower(b)
|
||||
})
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
@@ -24,3 +24,26 @@ func (b *Branch) ID() string {
|
||||
func (b *Branch) Description() string {
|
||||
return b.RefName()
|
||||
}
|
||||
|
||||
// this method does not consider the case where the git config states that a branch is tracking the config.
|
||||
// The Pullables value here is based on whether or not we saw an upstream when doing `git branch`
|
||||
func (b *Branch) IsTrackingRemote() bool {
|
||||
return b.IsRealBranch() && b.Pullables != "?"
|
||||
}
|
||||
|
||||
func (b *Branch) MatchesUpstream() bool {
|
||||
return b.IsRealBranch() && b.Pushables == "0" && b.Pullables == "0"
|
||||
}
|
||||
|
||||
func (b *Branch) HasCommitsToPush() bool {
|
||||
return b.IsRealBranch() && b.Pushables != "0"
|
||||
}
|
||||
|
||||
func (b *Branch) HasCommitsToPull() bool {
|
||||
return b.IsRealBranch() && b.Pullables != "0"
|
||||
}
|
||||
|
||||
// for when we're in a detached head state
|
||||
func (b *Branch) IsRealBranch() bool {
|
||||
return b.Pushables != "" && b.Pullables != ""
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ type Commit struct {
|
||||
Author string
|
||||
UnixTimestamp int64
|
||||
|
||||
// IsMerge tells us whether we're dealing with a merge commit i.e. a commit with two parents
|
||||
IsMerge bool
|
||||
// SHAs of parent commits (will be multiple if it's a merge commit)
|
||||
Parents []string
|
||||
}
|
||||
|
||||
func (c *Commit) ShortSha() string {
|
||||
@@ -35,3 +35,7 @@ func (c *Commit) ID() string {
|
||||
func (c *Commit) Description() string {
|
||||
return fmt.Sprintf("%s %s", c.Sha[:7], c.Name)
|
||||
}
|
||||
|
||||
func (c *Commit) IsMerge() bool {
|
||||
return len(c.Parents) > 1
|
||||
}
|
||||
|
||||
@@ -29,8 +29,6 @@ type IFile interface {
|
||||
GetPath() string
|
||||
}
|
||||
|
||||
const RENAME_SEPARATOR = " -> "
|
||||
|
||||
func (f *File) IsRename() bool {
|
||||
return f.PreviousName != ""
|
||||
}
|
||||
@@ -63,7 +61,7 @@ func (f *File) IsSubmodule(configs []*SubmoduleConfig) bool {
|
||||
|
||||
func (f *File) SubmoduleConfig(configs []*SubmoduleConfig) *SubmoduleConfig {
|
||||
for _, config := range configs {
|
||||
if f.Name == config.Name {
|
||||
if f.Name == config.Path {
|
||||
return config
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// A command object is a general way to represent a command to be run on the
|
||||
// command line. If you want to log the command you'll use .ToString() and
|
||||
// if you want to run it you'll use .GetCmd()
|
||||
type ICmdObj interface {
|
||||
GetCmd() *exec.Cmd
|
||||
ToString() string
|
||||
AddEnvVars(...string) ICmdObj
|
||||
}
|
||||
|
||||
type CmdObj struct {
|
||||
cmdStr string
|
||||
cmd *exec.Cmd
|
||||
}
|
||||
|
||||
func (self *CmdObj) GetCmd() *exec.Cmd {
|
||||
return self.cmd
|
||||
}
|
||||
|
||||
func (self *CmdObj) ToString() string {
|
||||
return self.cmdStr
|
||||
}
|
||||
|
||||
func (self *CmdObj) AddEnvVars(vars ...string) ICmdObj {
|
||||
self.cmd.Env = append(self.cmd.Env, vars...)
|
||||
|
||||
return self
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// 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(cmdObj ICmdObj, writer io.Writer, promptUserForCredential func(string) string) error {
|
||||
ttyText := ""
|
||||
errMessage := c.RunCommandWithOutputLive(cmdObj, writer, 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",
|
||||
}
|
||||
|
||||
for pattern, askFor := range prompts {
|
||||
if match, _ := regexp.MatchString(pattern, ttyText); match {
|
||||
ttyText = ""
|
||||
return promptUserForCredential(askFor)
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
})
|
||||
return errMessage
|
||||
}
|
||||
|
||||
// Due to a lack of pty support on windows we have RunCommandWithOutputLiveWrapper being defined
|
||||
// separate for windows and other OS's
|
||||
func (c *OSCommand) RunCommandWithOutputLive(cmdObj ICmdObj, writer io.Writer, handleOutput func(string) string) error {
|
||||
return RunCommandWithOutputLiveWrapper(c, cmdObj, writer, handleOutput)
|
||||
}
|
||||
|
||||
type cmdHandler struct {
|
||||
stdoutPipe io.Reader
|
||||
stdinPipe io.Writer
|
||||
close func() error
|
||||
}
|
||||
|
||||
// RunCommandWithOutputLiveAux runs a command and return every word that gets written in stdout
|
||||
// Output is a function that executes by every word that gets read by bufio
|
||||
// As return of output you need to give a string that will be written to stdin
|
||||
// NOTE: If the return data is empty it won't write anything to stdin
|
||||
func RunCommandWithOutputLiveAux(
|
||||
c *OSCommand,
|
||||
cmdObj ICmdObj,
|
||||
writer io.Writer,
|
||||
// handleOutput takes a word from stdout and returns a string to be written to stdin.
|
||||
// See DetectUnamePass above for how this is used to check for a username/password request
|
||||
handleOutput func(string) string,
|
||||
startCmd func(cmd *exec.Cmd) (*cmdHandler, error),
|
||||
) error {
|
||||
c.Log.WithField("command", cmdObj.ToString()).Info("RunCommand")
|
||||
c.LogCommand(cmdObj.ToString(), true)
|
||||
cmd := cmdObj.AddEnvVars("LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8").GetCmd()
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = io.MultiWriter(writer, &stderr)
|
||||
|
||||
handler, err := startCmd(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if closeErr := handler.close(); closeErr != nil {
|
||||
c.Log.Error(closeErr)
|
||||
}
|
||||
}()
|
||||
|
||||
tr := io.TeeReader(handler.stdoutPipe, writer)
|
||||
|
||||
go utils.Safe(func() {
|
||||
scanner := bufio.NewScanner(tr)
|
||||
scanner.Split(scanWordsWithNewLines)
|
||||
for scanner.Scan() {
|
||||
text := scanner.Text()
|
||||
output := strings.Trim(text, " ")
|
||||
toInput := handleOutput(output)
|
||||
if toInput != "" {
|
||||
_, _ = handler.stdinPipe.Write([]byte(toInput))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
return errors.New(stderr.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// scanWordsWithNewLines is a copy of bufio.ScanWords but this also captures new lines
|
||||
// For specific comments about this function take a look at: bufio.ScanWords
|
||||
func scanWordsWithNewLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
start := 0
|
||||
for width := 0; start < len(data); start += width {
|
||||
var r rune
|
||||
r, width = utf8.DecodeRune(data[start:])
|
||||
if !isSpace(r) {
|
||||
break
|
||||
}
|
||||
}
|
||||
for width, i := 0, start; i < len(data); i += width {
|
||||
var r rune
|
||||
r, width = utf8.DecodeRune(data[i:])
|
||||
if isSpace(r) {
|
||||
return i + width, data[start:i], nil
|
||||
}
|
||||
}
|
||||
if atEOF && len(data) > start {
|
||||
return len(data), data[start:], nil
|
||||
}
|
||||
return start, nil, nil
|
||||
}
|
||||
|
||||
// isSpace is also copied from the bufio package and has been modified to also captures new lines
|
||||
// For specific comments about this function take a look at: bufio.isSpace
|
||||
func isSpace(r rune) bool {
|
||||
if r <= '\u00FF' {
|
||||
switch r {
|
||||
case ' ', '\t', '\v', '\f':
|
||||
return true
|
||||
case '\u0085', '\u00A0':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
if '\u2000' <= r && r <= '\u200a' {
|
||||
return true
|
||||
}
|
||||
switch r {
|
||||
case '\u1680', '\u2028', '\u2029', '\u202f', '\u205f', '\u3000':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,98 +1,37 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"io"
|
||||
"os/exec"
|
||||
|
||||
"github.com/creack/pty"
|
||||
)
|
||||
|
||||
// RunCommandWithOutputLiveWrapper runs a command and return every word that gets written in stdout
|
||||
// Output is a function that executes by every word that gets read by bufio
|
||||
// As return of output you need to give a string that will be written to stdin
|
||||
// NOTE: If the return data is empty it won't written anything to stdin
|
||||
func RunCommandWithOutputLiveWrapper(c *OSCommand, command string, output func(string) string) error {
|
||||
c.Log.WithField("command", command).Info("RunCommand")
|
||||
c.LogCommand(command, true)
|
||||
cmd := c.ExecutableFromString(command)
|
||||
cmd.Env = append(cmd.Env, "LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8")
|
||||
func RunCommandWithOutputLiveWrapper(
|
||||
c *OSCommand,
|
||||
cmdObj ICmdObj,
|
||||
writer io.Writer,
|
||||
output func(string) string,
|
||||
) error {
|
||||
return RunCommandWithOutputLiveAux(
|
||||
c,
|
||||
cmdObj,
|
||||
writer,
|
||||
output,
|
||||
func(cmd *exec.Cmd) (*cmdHandler, error) {
|
||||
ptmx, err := pty.Start(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
ptmx, err := pty.Start(cmd)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go utils.Safe(func() {
|
||||
scanner := bufio.NewScanner(ptmx)
|
||||
scanner.Split(scanWordsWithNewLines)
|
||||
for scanner.Scan() {
|
||||
toOutput := strings.Trim(scanner.Text(), " ")
|
||||
_, _ = ptmx.WriteString(output(toOutput))
|
||||
}
|
||||
})
|
||||
|
||||
err = cmd.Wait()
|
||||
ptmx.Close()
|
||||
if err != nil {
|
||||
return errors.New(stderr.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// scanWordsWithNewLines is a copy of bufio.ScanWords but this also captures new lines
|
||||
// For specific comments about this function take a look at: bufio.ScanWords
|
||||
func scanWordsWithNewLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
start := 0
|
||||
for width := 0; start < len(data); start += width {
|
||||
var r rune
|
||||
r, width = utf8.DecodeRune(data[start:])
|
||||
if !isSpace(r) {
|
||||
break
|
||||
}
|
||||
}
|
||||
for width, i := 0, start; i < len(data); i += width {
|
||||
var r rune
|
||||
r, width = utf8.DecodeRune(data[i:])
|
||||
if isSpace(r) {
|
||||
return i + width, data[start:i], nil
|
||||
}
|
||||
}
|
||||
if atEOF && len(data) > start {
|
||||
return len(data), data[start:], nil
|
||||
}
|
||||
return start, nil, nil
|
||||
}
|
||||
|
||||
// isSpace is also copied from the bufio package and has been modified to also captures new lines
|
||||
// For specific comments about this function take a look at: bufio.isSpace
|
||||
func isSpace(r rune) bool {
|
||||
if r <= '\u00FF' {
|
||||
switch r {
|
||||
case ' ', '\t', '\v', '\f':
|
||||
return true
|
||||
case '\u0085', '\u00A0':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
if '\u2000' <= r && r <= '\u200a' {
|
||||
return true
|
||||
}
|
||||
switch r {
|
||||
case '\u1680', '\u2028', '\u2029', '\u202f', '\u205f', '\u3000':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return &cmdHandler{
|
||||
stdoutPipe: ptmx,
|
||||
stdinPipe: ptmx,
|
||||
close: ptmx.Close,
|
||||
}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,63 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os/exec"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Buffer struct {
|
||||
b bytes.Buffer
|
||||
m sync.Mutex
|
||||
}
|
||||
|
||||
func (b *Buffer) Read(p []byte) (n int, err error) {
|
||||
b.m.Lock()
|
||||
defer b.m.Unlock()
|
||||
return b.b.Read(p)
|
||||
}
|
||||
func (b *Buffer) Write(p []byte) (n int, err error) {
|
||||
b.m.Lock()
|
||||
defer b.m.Unlock()
|
||||
return b.b.Write(p)
|
||||
}
|
||||
|
||||
// RunCommandWithOutputLiveWrapper runs a command live but because of windows compatibility this command can't be ran there
|
||||
// TODO: Remove this hack and replace it with a proper way to run commands live on windows
|
||||
func RunCommandWithOutputLiveWrapper(c *OSCommand, command string, output func(string) string) error {
|
||||
return c.RunCommand(command)
|
||||
// TODO: Remove this hack and replace it with a proper way to run commands live on windows. We still have an issue where if a password is requested, the request for a password is written straight to stdout because we can't control the stdout of a subprocess of a subprocess. Keep an eye on https://github.com/creack/pty/pull/109
|
||||
func RunCommandWithOutputLiveWrapper(
|
||||
c *OSCommand,
|
||||
cmdObj ICmdObj,
|
||||
writer io.Writer,
|
||||
output func(string) string,
|
||||
) error {
|
||||
return RunCommandWithOutputLiveAux(
|
||||
c,
|
||||
cmdObj,
|
||||
writer,
|
||||
output,
|
||||
func(cmd *exec.Cmd) (*cmdHandler, error) {
|
||||
stdoutReader, stdoutWriter := io.Pipe()
|
||||
cmd.Stdout = stdoutWriter
|
||||
|
||||
buf := &Buffer{}
|
||||
cmd.Stdin = buf
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// because we don't yet have windows support for a pty, we instead just
|
||||
// pass our standard stream handlers and because there's no pty to close
|
||||
// we pass a no-op function for that.
|
||||
return &cmdHandler{
|
||||
stdoutPipe: stdoutReader,
|
||||
stdinPipe: buf,
|
||||
close: func() error { return nil },
|
||||
}, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -24,10 +23,8 @@ import (
|
||||
// Platform stores the os state
|
||||
type Platform struct {
|
||||
OS string
|
||||
CatCmd []string
|
||||
Shell string
|
||||
ShellArg string
|
||||
EscapedQuote string
|
||||
OpenCommand string
|
||||
OpenLinkCommand string
|
||||
}
|
||||
@@ -203,7 +200,14 @@ 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
|
||||
quotedCommand = strings.NewReplacer(
|
||||
"^", "^^",
|
||||
"&", "^&",
|
||||
"|", "^|",
|
||||
"<", "^<",
|
||||
">", "^>",
|
||||
"%", "^%",
|
||||
).Replace(commandStr)
|
||||
} else {
|
||||
quotedCommand = c.Quote(commandStr)
|
||||
}
|
||||
@@ -212,50 +216,6 @@ func (c *OSCommand) ShellCommandFromString(commandStr string) *exec.Cmd {
|
||||
return c.ExecutableFromString(shellCommand)
|
||||
}
|
||||
|
||||
// RunCommandWithOutputLive runs RunCommandWithOutputLiveWrapper
|
||||
func (c *OSCommand) RunCommandWithOutputLive(command string, output func(string) string) error {
|
||||
return RunCommandWithOutputLiveWrapper(c, command, output)
|
||||
}
|
||||
|
||||
func (c *OSCommand) CatFile(filename string) (string, error) {
|
||||
arr := append(c.Platform.CatCmd, filename)
|
||||
cmdStr := strings.Join(arr, " ")
|
||||
c.Log.WithField("command", cmdStr).Info("Cat")
|
||||
cmd := c.Command(arr[0], arr[1:]...)
|
||||
output, err := sanitisedCommandOutput(cmd.CombinedOutput())
|
||||
if err != nil {
|
||||
c.Log.WithField("command", cmdStr).Error(output)
|
||||
}
|
||||
return output, err
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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",
|
||||
}
|
||||
|
||||
for pattern, askFor := range prompts {
|
||||
if match, _ := regexp.MatchString(pattern, ttyText); match {
|
||||
ttyText = ""
|
||||
return promptUserForCredential(askFor)
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
})
|
||||
return errMessage
|
||||
}
|
||||
|
||||
// RunCommand runs a command and just returns the error
|
||||
func (c *OSCommand) RunCommand(formatString string, formatArgs ...interface{}) error {
|
||||
_, err := c.RunCommandWithOutput(formatString, formatArgs...)
|
||||
@@ -265,7 +225,7 @@ func (c *OSCommand) RunCommand(formatString string, formatArgs ...interface{}) e
|
||||
// RunShellCommand runs shell commands i.e. 'sh -c <command>'. Good for when you
|
||||
// need access to the shell
|
||||
func (c *OSCommand) RunShellCommand(command string) error {
|
||||
cmd := c.Command(c.Platform.Shell, c.Platform.ShellArg, command)
|
||||
cmd := c.ShellCommandFromString(command)
|
||||
c.LogExecCmd(cmd)
|
||||
|
||||
_, err := sanitisedCommandOutput(cmd.CombinedOutput())
|
||||
@@ -304,9 +264,8 @@ func (c *OSCommand) OpenFile(filename string) error {
|
||||
templateValues := map[string]string{
|
||||
"filename": c.Quote(filename),
|
||||
}
|
||||
|
||||
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
|
||||
err := c.RunCommand(command)
|
||||
err := c.RunShellCommand(command)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -319,7 +278,7 @@ func (c *OSCommand) OpenLink(link string) error {
|
||||
}
|
||||
|
||||
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
|
||||
err := c.RunCommand(command)
|
||||
err := c.RunShellCommand(command)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -341,17 +300,23 @@ func (c *OSCommand) PrepareShellSubProcess(command string) *exec.Cmd {
|
||||
|
||||
// Quote wraps a message in platform-specific quotation marks
|
||||
func (c *OSCommand) Quote(message string) string {
|
||||
var quote string
|
||||
if c.Platform.OS == "windows" {
|
||||
message = strings.Replace(message, `"`, `"'"'"`, -1)
|
||||
message = strings.Replace(message, `\"`, `\\"`, -1)
|
||||
quote = `\"`
|
||||
message = strings.NewReplacer(
|
||||
`"`, `"'"'"`,
|
||||
`\"`, `\\"`,
|
||||
).Replace(message)
|
||||
} else {
|
||||
message = strings.Replace(message, `\`, `\\`, -1)
|
||||
message = strings.Replace(message, `"`, `\"`, -1)
|
||||
message = strings.Replace(message, "`", "\\`", -1)
|
||||
message = strings.Replace(message, "$", "\\$", -1)
|
||||
quote = `"`
|
||||
message = strings.NewReplacer(
|
||||
`\`, `\\`,
|
||||
`"`, `\"`,
|
||||
`$`, `\$`,
|
||||
"`", "\\`",
|
||||
).Replace(message)
|
||||
}
|
||||
escapedQuote := c.Platform.EscapedQuote
|
||||
return escapedQuote + message + escapedQuote
|
||||
return quote + message + quote
|
||||
}
|
||||
|
||||
// AppendLineToFile adds a new line in file
|
||||
@@ -554,7 +519,9 @@ func RunLineOutputCmd(cmd *exec.Cmd, onLine func(line string) (bool, error)) err
|
||||
}
|
||||
|
||||
func (c *OSCommand) CopyToClipboard(str string) error {
|
||||
c.LogCommand(fmt.Sprintf("Copying '%s' to clipboard", utils.TruncateWithEllipsis(str, 40)), false)
|
||||
escaped := strings.Replace(str, "\n", "\\n", -1)
|
||||
truncated := utils.TruncateWithEllipsis(escaped, 40)
|
||||
c.LogCommand(fmt.Sprintf("Copying '%s' to clipboard", truncated), false)
|
||||
return clipboard.WriteAll(str)
|
||||
}
|
||||
|
||||
@@ -563,3 +530,30 @@ func (c *OSCommand) RemoveFile(path string) error {
|
||||
|
||||
return c.removeFile(path)
|
||||
}
|
||||
|
||||
func (c *OSCommand) NewCmdObjFromStr(cmdStr string) ICmdObj {
|
||||
args := str.ToArgv(cmdStr)
|
||||
cmd := c.Command(args[0], args[1:]...)
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
return &CmdObj{
|
||||
cmdStr: cmdStr,
|
||||
cmd: cmd,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *OSCommand) NewCmdObjFromArgs(args []string) ICmdObj {
|
||||
cmd := c.Command(args[0], args[1:]...)
|
||||
|
||||
return &CmdObj{
|
||||
cmdStr: strings.Join(args, " "),
|
||||
cmd: cmd,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *OSCommand) NewCmdObj(cmd *exec.Cmd) ICmdObj {
|
||||
return &CmdObj{
|
||||
cmdStr: strings.Join(cmd.Args, " "),
|
||||
cmd: cmd,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package oscommands
|
||||
@@ -9,10 +10,8 @@ import (
|
||||
func getPlatform() *Platform {
|
||||
return &Platform{
|
||||
OS: runtime.GOOS,
|
||||
CatCmd: []string{"cat"},
|
||||
Shell: "bash",
|
||||
ShellArg: "-c",
|
||||
EscapedQuote: `"`,
|
||||
OpenCommand: "open {{filename}}",
|
||||
OpenLinkCommand: "open {{link}}",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestOSCommandOpenFileDarwin is a function.
|
||||
func TestOSCommandOpenFileDarwin(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
return secureexec.Command("exit", "1")
|
||||
},
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "bash", name)
|
||||
assert.Equal(t, []string{"-c", `open "test"`}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"filename with spaces",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "bash", name)
|
||||
assert.Equal(t, []string{"-c", `open "filename with spaces"`}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
OSCmd := NewDummyOSCommand()
|
||||
OSCmd.Platform.OS = "darwin"
|
||||
OSCmd.Command = s.command
|
||||
OSCmd.Config.GetUserConfig().OS.OpenCommand = "open {{filename}}"
|
||||
|
||||
s.test(OSCmd.OpenFile(s.filename))
|
||||
}
|
||||
}
|
||||
|
||||
// TestOSCommandOpenFileLinux tests the OpenFile command on Linux
|
||||
func TestOSCommandOpenFileLinux(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
return secureexec.Command("exit", "1")
|
||||
},
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "bash", name)
|
||||
assert.Equal(t, []string{"-c", `xdg-open "test" > /dev/null`}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"filename with spaces",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "bash", name)
|
||||
assert.Equal(t, []string{"-c", `xdg-open "filename with spaces" > /dev/null`}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"let's_test_with_single_quote",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "bash", name)
|
||||
assert.Equal(t, []string{"-c", `xdg-open "let's_test_with_single_quote" > /dev/null`}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"$USER.txt",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "bash", name)
|
||||
assert.Equal(t, []string{"-c", `xdg-open "\$USER.txt" > /dev/null`}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
OSCmd := NewDummyOSCommand()
|
||||
OSCmd.Command = s.command
|
||||
OSCmd.Platform.OS = "linux"
|
||||
OSCmd.Config.GetUserConfig().OS.OpenCommand = `xdg-open {{filename}} > /dev/null`
|
||||
|
||||
s.test(OSCmd.OpenFile(s.filename))
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,8 @@ package oscommands
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -59,57 +57,6 @@ func TestOSCommandRunCommand(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestOSCommandOpenFile is a function.
|
||||
func TestOSCommandOpenFile(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
return secureexec.Command("exit", "1")
|
||||
},
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "open", name)
|
||||
assert.Equal(t, []string{"test"}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"filename with spaces",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "open", name)
|
||||
assert.Equal(t, []string{"filename with spaces"}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
OSCmd := NewDummyOSCommand()
|
||||
OSCmd.Command = s.command
|
||||
OSCmd.Config.GetUserConfig().OS.OpenCommand = "open {{filename}}"
|
||||
|
||||
s.test(OSCmd.OpenFile(s.filename))
|
||||
}
|
||||
}
|
||||
|
||||
// TestOSCommandQuote is a function.
|
||||
func TestOSCommandQuote(t *testing.T) {
|
||||
osCommand := NewDummyOSCommand()
|
||||
@@ -118,7 +65,7 @@ func TestOSCommandQuote(t *testing.T) {
|
||||
|
||||
actual := osCommand.Quote("hello `test`")
|
||||
|
||||
expected := osCommand.Platform.EscapedQuote + "hello \\`test\\`" + osCommand.Platform.EscapedQuote
|
||||
expected := "\"hello \\`test\\`\""
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
@@ -131,7 +78,7 @@ func TestOSCommandQuoteSingleQuote(t *testing.T) {
|
||||
|
||||
actual := osCommand.Quote("hello 'test'")
|
||||
|
||||
expected := osCommand.Platform.EscapedQuote + "hello 'test'" + osCommand.Platform.EscapedQuote
|
||||
expected := `"hello 'test'"`
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
@@ -144,7 +91,7 @@ func TestOSCommandQuoteDoubleQuote(t *testing.T) {
|
||||
|
||||
actual := osCommand.Quote(`hello "test"`)
|
||||
|
||||
expected := osCommand.Platform.EscapedQuote + `hello \"test\"` + osCommand.Platform.EscapedQuote
|
||||
expected := `"hello \"test\""`
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
@@ -155,9 +102,9 @@ func TestOSCommandQuoteWindows(t *testing.T) {
|
||||
|
||||
osCommand.Platform.OS = "windows"
|
||||
|
||||
actual := osCommand.Quote(`hello "test"`)
|
||||
actual := osCommand.Quote(`hello "test" 'test2'`)
|
||||
|
||||
expected := osCommand.Platform.EscapedQuote + `hello "'"'"test"'"'"` + osCommand.Platform.EscapedQuote
|
||||
expected := `\"hello "'"'"test"'"'" 'test2'\"`
|
||||
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,8 @@ package oscommands
|
||||
|
||||
func getPlatform() *Platform {
|
||||
return &Platform{
|
||||
OS: "windows",
|
||||
CatCmd: []string{"cmd", "/c", "type"},
|
||||
Shell: "cmd",
|
||||
ShellArg: "/c",
|
||||
EscapedQuote: `\"`,
|
||||
OS: "windows",
|
||||
Shell: "cmd",
|
||||
ShellArg: "/c",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestOSCommandOpenFileWindows tests the OpenFile command on Linux
|
||||
func TestOSCommandOpenFileWindows(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
return secureexec.Command("exit", "1")
|
||||
},
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "cmd", name)
|
||||
assert.Equal(t, []string{"/c", "start", "", "test"}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"filename with spaces",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "cmd", name)
|
||||
assert.Equal(t, []string{"/c", "start", "", "filename with spaces"}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"let's_test_with_single_quote",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "cmd", name)
|
||||
assert.Equal(t, []string{"/c", "start", "", "let's_test_with_single_quote"}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"$USER.txt",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "cmd", name)
|
||||
assert.Equal(t, []string{"/c", "start", "", "$USER.txt"}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
OSCmd := NewDummyOSCommand()
|
||||
OSCmd.Command = s.command
|
||||
OSCmd.Platform.OS = "windows"
|
||||
OSCmd.Config.GetUserConfig().OS.OpenCommand = `start "" {{filename}}`
|
||||
|
||||
s.test(OSCmd.OpenFile(s.filename))
|
||||
}
|
||||
}
|
||||
@@ -138,7 +138,14 @@ func ModifiedPatchForLines(log *logrus.Entry, filename string, diffText string,
|
||||
|
||||
// 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]
|
||||
n := idx - hunk.FirstLineIdx - 1
|
||||
if n < 0 {
|
||||
n = 0
|
||||
} else if n >= len(hunk.bodyLines) {
|
||||
n = len(hunk.bodyLines) - 1
|
||||
}
|
||||
|
||||
lines := hunk.bodyLines[0:n]
|
||||
|
||||
offset := nLinesWithPrefix(lines, []string{"+", " "})
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -95,45 +95,39 @@ func (l *PatchLine) render(selected bool, included bool) string {
|
||||
if l.Kind == HUNK_HEADER {
|
||||
re := regexp.MustCompile("(@@.*?@@)(.*)")
|
||||
match := re.FindStringSubmatch(content)
|
||||
return coloredString(color.FgCyan, match[1], selected, included) + coloredString(theme.DefaultTextColor, match[2], selected, false)
|
||||
return coloredString(style.FgCyan, match[1], selected, included) + coloredString(theme.DefaultTextColor, match[2], selected, false)
|
||||
}
|
||||
|
||||
var colorAttr color.Attribute
|
||||
textStyle := theme.DefaultTextColor
|
||||
switch l.Kind {
|
||||
case PATCH_HEADER:
|
||||
colorAttr = color.Bold
|
||||
textStyle = textStyle.SetBold()
|
||||
case ADDITION:
|
||||
colorAttr = color.FgGreen
|
||||
textStyle = style.FgGreen
|
||||
case DELETION:
|
||||
colorAttr = color.FgRed
|
||||
textStyle = style.FgRed
|
||||
case COMMIT_SHA:
|
||||
colorAttr = color.FgYellow
|
||||
default:
|
||||
colorAttr = theme.DefaultTextColor
|
||||
textStyle = style.FgYellow
|
||||
}
|
||||
|
||||
return coloredString(colorAttr, content, selected, included)
|
||||
return coloredString(textStyle, content, selected, included)
|
||||
}
|
||||
|
||||
func coloredString(colorAttr color.Attribute, str string, selected bool, included bool) string {
|
||||
var cl *color.Color
|
||||
attributes := []color.Attribute{colorAttr}
|
||||
func coloredString(textStyle style.TextStyle, str string, selected bool, included bool) string {
|
||||
if selected {
|
||||
attributes = append(attributes, theme.SelectedRangeBgColor)
|
||||
textStyle = textStyle.MergeStyle(theme.SelectedRangeBgColor)
|
||||
}
|
||||
cl = color.New(attributes...)
|
||||
var clIncluded *color.Color
|
||||
|
||||
firstCharStyle := textStyle
|
||||
if included {
|
||||
clIncluded = color.New(append(attributes, color.BgGreen)...)
|
||||
} else {
|
||||
clIncluded = color.New(attributes...)
|
||||
firstCharStyle = firstCharStyle.MergeStyle(style.BgGreen)
|
||||
}
|
||||
|
||||
if len(str) < 2 {
|
||||
return utils.ColoredStringDirect(str, clIncluded)
|
||||
return firstCharStyle.Sprint(str)
|
||||
}
|
||||
|
||||
return utils.ColoredStringDirect(str[:1], clIncluded) + utils.ColoredStringDirect(str[1:], cl)
|
||||
return firstCharStyle.Sprint(str[:1]) + textStyle.Sprint(str[1:])
|
||||
}
|
||||
|
||||
func parsePatch(patch string) ([]int, []int, []*PatchLine) {
|
||||
@@ -200,6 +194,19 @@ func (p *PatchParser) Render(firstLineIndex int, lastLineIndex int, incLineIndic
|
||||
return result
|
||||
}
|
||||
|
||||
// PlainRenderLines returns the non-coloured string of diff part from firstLineIndex to
|
||||
// lastLineIndex
|
||||
func (p *PatchParser) PlainRenderLines(firstLineIndex, lastLineIndex int) string {
|
||||
linesToCopy := p.PatchLines[firstLineIndex : lastLineIndex+1]
|
||||
|
||||
renderedLines := make([]string, len(linesToCopy))
|
||||
for index, line := range linesToCopy {
|
||||
renderedLines[index] = line.Content
|
||||
}
|
||||
|
||||
return strings.Join(renderedLines, "\n")
|
||||
}
|
||||
|
||||
// GetNextStageableLineIndex takes a line index and returns the line index of the next stageable line
|
||||
// note this will actually include the current index if it is stageable
|
||||
func (p *PatchParser) GetNextStageableLineIndex(currentIndex int) int {
|
||||
|
||||
@@ -5,14 +5,62 @@ 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, ...)
|
||||
type Service struct {
|
||||
Name string
|
||||
PullRequestURL string
|
||||
Name string
|
||||
pullRequestURLIntoDefaultBranch func(owner string, repository string, from string) string
|
||||
pullRequestURLIntoTargetBranch func(owner string, repository string, from string, to string) 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,
|
||||
pullRequestURLIntoDefaultBranch: func(owner string, repository string, from string) string {
|
||||
return fmt.Sprintf("https://%s/%s/%s/compare/%s?expand=1", siteDomain, owner, repository, from)
|
||||
},
|
||||
pullRequestURLIntoTargetBranch: func(owner string, repository string, from string, to string) string {
|
||||
return fmt.Sprintf("https://%s/%s/%s/compare/%s...%s?expand=1", siteDomain, owner, repository, to, from)
|
||||
},
|
||||
}
|
||||
case "bitbucket":
|
||||
service = &Service{
|
||||
Name: repositoryDomain,
|
||||
pullRequestURLIntoDefaultBranch: func(owner string, repository string, from string) string {
|
||||
return fmt.Sprintf("https://%s/%s/%s/pull-requests/new?source=%s&t=1", siteDomain, owner, repository, from)
|
||||
},
|
||||
pullRequestURLIntoTargetBranch: func(owner string, repository string, from string, to string) string {
|
||||
return fmt.Sprintf("https://%s/%s/%s/pull-requests/new?source=%s&dest=%s&t=1", siteDomain, owner, repository, from, to)
|
||||
},
|
||||
}
|
||||
case "gitlab":
|
||||
service = &Service{
|
||||
Name: repositoryDomain,
|
||||
pullRequestURLIntoDefaultBranch: func(owner string, repository string, from string) string {
|
||||
return fmt.Sprintf("https://%s/%s/%s/merge_requests/new?merge_request[source_branch]=%s", siteDomain, owner, repository, from)
|
||||
},
|
||||
pullRequestURLIntoTargetBranch: func(owner string, repository string, from string, to string) string {
|
||||
return fmt.Sprintf("https://%s/%s/%s/merge_requests/new?merge_request[source_branch]=%s&merge_request[target_branch]=%s", siteDomain, owner, repository, from, to)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
func (s *Service) PullRequestURL(owner string, repository string, from string, to string) string {
|
||||
if to == "" {
|
||||
return s.pullRequestURLIntoDefaultBranch(owner, repository, from)
|
||||
} else {
|
||||
return s.pullRequestURLIntoTargetBranch(owner, repository, from, to)
|
||||
}
|
||||
}
|
||||
|
||||
// PullRequest opens a link in browser to create new pull request
|
||||
@@ -28,31 +76,6 @@ 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"),
|
||||
}
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
func getServices(config config.AppConfigurer) []*Service {
|
||||
services := []*Service{
|
||||
NewService("github", "github.com", "github.com"),
|
||||
@@ -90,8 +113,8 @@ func NewPullRequest(gitCommand *GitCommand) *PullRequest {
|
||||
}
|
||||
|
||||
// Create opens link to new pull request in browser
|
||||
func (pr *PullRequest) Create(branch *models.Branch) (string, error) {
|
||||
pullRequestURL, err := pr.getPullRequestURL(branch)
|
||||
func (pr *PullRequest) Create(from string, to string) (string, error) {
|
||||
pullRequestURL, err := pr.getPullRequestURL(from, to)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -100,8 +123,8 @@ func (pr *PullRequest) Create(branch *models.Branch) (string, error) {
|
||||
}
|
||||
|
||||
// CopyURL copies the pull request URL to the clipboard
|
||||
func (pr *PullRequest) CopyURL(branch *models.Branch) (string, error) {
|
||||
pullRequestURL, err := pr.getPullRequestURL(branch)
|
||||
func (pr *PullRequest) CopyURL(from string, to string) (string, error) {
|
||||
pullRequestURL, err := pr.getPullRequestURL(from, to)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -109,8 +132,8 @@ func (pr *PullRequest) CopyURL(branch *models.Branch) (string, error) {
|
||||
return pullRequestURL, pr.GitCommand.OSCommand.CopyToClipboard(pullRequestURL)
|
||||
}
|
||||
|
||||
func (pr *PullRequest) getPullRequestURL(branch *models.Branch) (string, error) {
|
||||
branchExistsOnRemote := pr.GitCommand.CheckRemoteBranchExists(branch)
|
||||
func (pr *PullRequest) getPullRequestURL(from string, to string) (string, error) {
|
||||
branchExistsOnRemote := pr.GitCommand.CheckRemoteBranchExists(from)
|
||||
|
||||
if !branchExistsOnRemote {
|
||||
return "", errors.New(pr.GitCommand.Tr.NoBranchOnRemote)
|
||||
@@ -131,9 +154,8 @@ func (pr *PullRequest) getPullRequestURL(branch *models.Branch) (string, error)
|
||||
}
|
||||
|
||||
repoInfo := getRepoInfoFromURL(repoURL)
|
||||
pullRequestURL := fmt.Sprintf(
|
||||
gitService.PullRequestURL, repoInfo.Owner, repoInfo.Repository, branch.Name,
|
||||
)
|
||||
|
||||
pullRequestURL := gitService.PullRequestURL(repoInfo.Owner, repoInfo.Repository, from, to)
|
||||
|
||||
return pullRequestURL, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestCreatePullRequest is a function.
|
||||
func TestCreatePullRequest(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
from string
|
||||
to string
|
||||
remoteUrl string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(url string, err error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket",
|
||||
from: "feature/profile-page",
|
||||
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "bash")
|
||||
assert.Equal(t, args, []string{"-c", `open "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1"`})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket with http remote url",
|
||||
from: "feature/events",
|
||||
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "bash")
|
||||
assert.Equal(t, args, []string{"-c", `open "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1"`})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on github",
|
||||
from: "feature/sum-operation",
|
||||
remoteUrl: "git@github.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@github.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "bash")
|
||||
assert.Equal(t, args, []string{"-c", `open "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1"`})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket with specific target branch",
|
||||
from: "feature/profile-page/avatar",
|
||||
to: "feature/profile-page",
|
||||
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "bash")
|
||||
assert.Equal(t, args, []string{"-c", `open "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar&dest=feature/profile-page&t=1"`})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar&dest=feature/profile-page&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket with http remote url with specified target branch",
|
||||
from: "feature/remote-events",
|
||||
to: "feature/events",
|
||||
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "bash")
|
||||
assert.Equal(t, args, []string{"-c", `open "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events&dest=feature/events&t=1"`})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events&dest=feature/events&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on github with specific target branch",
|
||||
from: "feature/sum-operation",
|
||||
to: "feature/operations",
|
||||
remoteUrl: "git@github.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@github.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "bash")
|
||||
assert.Equal(t, args, []string{"-c", `open "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1"`})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab",
|
||||
from: "feature/ui",
|
||||
remoteUrl: "git@gitlab.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "bash")
|
||||
assert.Equal(t, args, []string{"-c", `open "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"`})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab in nested groups",
|
||||
from: "feature/ui",
|
||||
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "bash")
|
||||
assert.Equal(t, args, []string{"-c", `open "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"`})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab with specific target branch",
|
||||
from: "feature/commit-ui",
|
||||
to: "epic/ui",
|
||||
remoteUrl: "git@gitlab.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "bash")
|
||||
assert.Equal(t, args, []string{"-c", `open "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui"`})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab with specific target branch in nested groups",
|
||||
from: "feature/commit-ui",
|
||||
to: "epic/ui",
|
||||
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "bash")
|
||||
assert.Equal(t, args, []string{"-c", `open "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui"`})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Throws an error if git service is unsupported",
|
||||
from: "feature/divide-operation",
|
||||
remoteUrl: "git@something.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCommand := NewDummyGitCommand()
|
||||
gitCommand.OSCommand.Command = s.command
|
||||
gitCommand.OSCommand.Platform.OS = "darwin"
|
||||
gitCommand.OSCommand.Platform.Shell = "bash"
|
||||
gitCommand.OSCommand.Platform.ShellArg = "-c"
|
||||
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.GitConfig = git_config.NewFakeGitConfig(map[string]string{"remote.origin.url": s.remoteUrl})
|
||||
dummyPullRequest := NewPullRequest(gitCommand)
|
||||
s.test(dummyPullRequest.Create(s.from, s.to))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,8 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -43,135 +39,3 @@ func TestGetRepoInfoFromURL(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreatePullRequest is a function.
|
||||
func TestCreatePullRequest(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
branch *models.Branch
|
||||
remoteUrl string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(url string, err error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket",
|
||||
branch: &models.Branch{
|
||||
Name: "feature/profile-page",
|
||||
},
|
||||
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "open")
|
||||
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket with http remote url",
|
||||
branch: &models.Branch{
|
||||
Name: "feature/events",
|
||||
},
|
||||
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "open")
|
||||
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on github",
|
||||
branch: &models.Branch{
|
||||
Name: "feature/sum-operation",
|
||||
},
|
||||
remoteUrl: "git@github.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@github.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "open")
|
||||
assert.Equal(t, args, []string{"https://github.com/peter/calculator/compare/feature/sum-operation?expand=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab",
|
||||
branch: &models.Branch{
|
||||
Name: "feature/ui",
|
||||
},
|
||||
remoteUrl: "git@gitlab.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "open")
|
||||
assert.Equal(t, args, []string{"https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Throws an error if git service is unsupported",
|
||||
branch: &models.Branch{
|
||||
Name: "feature/divide-operation",
|
||||
},
|
||||
remoteUrl: "git@something.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCommand := NewDummyGitCommand()
|
||||
gitCommand.OSCommand.Command = s.command
|
||||
gitCommand.OSCommand.Config.GetUserConfig().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.getGitConfigValue = func(path string) (string, error) {
|
||||
assert.Equal(t, path, "remote.origin.url")
|
||||
return s.remoteUrl, nil
|
||||
}
|
||||
dummyPullRequest := NewPullRequest(gitCommand)
|
||||
s.test(dummyPullRequest.Create(s.branch))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestCreatePullRequestOnWindows is a function.
|
||||
func TestCreatePullRequestOnWindows(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
from string
|
||||
to string
|
||||
remoteUrl string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(url string, err error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket",
|
||||
from: "feature/profile-page",
|
||||
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "cmd")
|
||||
assert.Equal(t, args, []string{"/c", "start", "", "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page^&t=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket with http remote url",
|
||||
from: "feature/events",
|
||||
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "cmd")
|
||||
assert.Equal(t, args, []string{"/c", "start", "", "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events^&t=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on github",
|
||||
from: "feature/sum-operation",
|
||||
remoteUrl: "git@github.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@github.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "cmd")
|
||||
assert.Equal(t, args, []string{"/c", "start", "", "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket with specific target branch",
|
||||
from: "feature/profile-page/avatar",
|
||||
to: "feature/profile-page",
|
||||
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "cmd")
|
||||
assert.Equal(t, args, []string{"/c", "start", "", "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar^&dest=feature/profile-page^&t=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar&dest=feature/profile-page&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket with http remote url with specified target branch",
|
||||
from: "feature/remote-events",
|
||||
to: "feature/events",
|
||||
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "cmd")
|
||||
assert.Equal(t, args, []string{"/c", "start", "", "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events^&dest=feature/events^&t=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events&dest=feature/events&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on github with specific target branch",
|
||||
from: "feature/sum-operation",
|
||||
to: "feature/operations",
|
||||
remoteUrl: "git@github.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@github.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "cmd")
|
||||
assert.Equal(t, args, []string{"/c", "start", "", "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab",
|
||||
from: "feature/ui",
|
||||
remoteUrl: "git@gitlab.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "cmd")
|
||||
assert.Equal(t, args, []string{"/c", "start", "", "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab in nested groups",
|
||||
from: "feature/ui",
|
||||
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "cmd")
|
||||
assert.Equal(t, args, []string{"/c", "start", "", "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab with specific target branch",
|
||||
from: "feature/commit-ui",
|
||||
to: "epic/ui",
|
||||
remoteUrl: "git@gitlab.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "cmd")
|
||||
assert.Equal(t, args, []string{"/c", "start", "", "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui^&merge_request[target_branch]=epic/ui"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab with specific target branch in nested groups",
|
||||
from: "feature/commit-ui",
|
||||
to: "epic/ui",
|
||||
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "cmd")
|
||||
assert.Equal(t, args, []string{"/c", "start", "", "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui^&merge_request[target_branch]=epic/ui"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Throws an error if git service is unsupported",
|
||||
from: "feature/divide-operation",
|
||||
remoteUrl: "git@something.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCommand := NewDummyGitCommand()
|
||||
gitCommand.OSCommand.Command = s.command
|
||||
gitCommand.OSCommand.Platform.OS = "windows"
|
||||
gitCommand.OSCommand.Platform.Shell = "cmd"
|
||||
gitCommand.OSCommand.Platform.ShellArg = "/c"
|
||||
gitCommand.OSCommand.Config.GetUserConfig().OS.OpenLinkCommand = `start "" {{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.GitConfig = git_config.NewFakeGitConfig(map[string]string{"remote.origin.url": s.remoteUrl})
|
||||
dummyPullRequest := NewPullRequest(gitCommand)
|
||||
s.test(dummyPullRequest.Create(s.from, s.to))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -119,7 +119,7 @@ func (c *GitCommand) GenerateGenericRebaseTodo(commits []*models.Commit, actionI
|
||||
var commitAction string
|
||||
if i == actionIndex {
|
||||
commitAction = action
|
||||
} else if commit.IsMerge {
|
||||
} 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.
|
||||
|
||||
+15
-10
@@ -3,35 +3,40 @@ package commands
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
)
|
||||
|
||||
func (c *GitCommand) AddRemote(name string, url string) error {
|
||||
return c.RunCommand("git remote add %s %s", name, url)
|
||||
return c.RunCommand("git remote add %s %s", c.OSCommand.Quote(name), c.OSCommand.Quote(url))
|
||||
}
|
||||
|
||||
func (c *GitCommand) RemoveRemote(name string) error {
|
||||
return c.RunCommand("git remote remove %s", name)
|
||||
return c.RunCommand("git remote remove %s", c.OSCommand.Quote(name))
|
||||
}
|
||||
|
||||
func (c *GitCommand) RenameRemote(oldRemoteName string, newRemoteName string) error {
|
||||
return c.RunCommand("git remote rename %s %s", oldRemoteName, newRemoteName)
|
||||
return c.RunCommand("git remote rename %s %s", c.OSCommand.Quote(oldRemoteName), c.OSCommand.Quote(newRemoteName))
|
||||
}
|
||||
|
||||
func (c *GitCommand) UpdateRemoteUrl(remoteName string, updatedUrl string) error {
|
||||
return c.RunCommand("git remote set-url %s %s", remoteName, updatedUrl)
|
||||
return c.RunCommand("git remote set-url %s %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(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)
|
||||
command := fmt.Sprintf("git push %s --delete %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(branchName))
|
||||
cmdObj := c.NewCmdObjFromStr(command)
|
||||
return c.DetectUnamePass(cmdObj, promptUserForCredential)
|
||||
}
|
||||
|
||||
func (c *GitCommand) DetectUnamePass(cmdObj oscommands.ICmdObj, promptUserForCredential func(string) string) error {
|
||||
return c.OSCommand.DetectUnamePass(cmdObj, c.GetCmdWriter(), promptUserForCredential)
|
||||
}
|
||||
|
||||
// CheckRemoteBranchExists Returns remote branch
|
||||
func (c *GitCommand) CheckRemoteBranchExists(branch *models.Branch) bool {
|
||||
func (c *GitCommand) CheckRemoteBranchExists(branchName string) bool {
|
||||
_, err := c.OSCommand.RunCommandWithOutput(
|
||||
"git show-ref --verify -- refs/remotes/origin/%s",
|
||||
branch.Name,
|
||||
c.OSCommand.Quote(branchName),
|
||||
)
|
||||
|
||||
return err == nil
|
||||
@@ -39,5 +44,5 @@ func (c *GitCommand) CheckRemoteBranchExists(branch *models.Branch) bool {
|
||||
|
||||
// GetRemoteURL returns current repo remote url
|
||||
func (c *GitCommand) GetRemoteURL() string {
|
||||
return c.GetConfigValue("remote.origin.url")
|
||||
return c.GitConfig.Get("remote.origin.url")
|
||||
}
|
||||
|
||||
@@ -69,11 +69,11 @@ func (c *GitCommand) SubmoduleStash(submodule *models.SubmoduleConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.RunCommand("git -C %s stash --include-untracked", submodule.Path)
|
||||
return c.RunCommand("git -C %s stash --include-untracked", c.OSCommand.Quote(submodule.Path))
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleReset(submodule *models.SubmoduleConfig) error {
|
||||
return c.RunCommand("git submodule update --init --force %s", submodule.Path)
|
||||
return c.RunCommand("git submodule update --init --force -- %s", c.OSCommand.Quote(submodule.Path))
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleUpdateAll() error {
|
||||
@@ -84,13 +84,13 @@ func (c *GitCommand) SubmoduleUpdateAll() error {
|
||||
func (c *GitCommand) SubmoduleDelete(submodule *models.SubmoduleConfig) error {
|
||||
// based on https://gist.github.com/myusuf3/7f645819ded92bda6677
|
||||
|
||||
if err := c.RunCommand("git submodule deinit --force %s", submodule.Path); err != nil {
|
||||
if err := c.RunCommand("git submodule deinit --force -- %s", c.OSCommand.Quote(submodule.Path)); err != nil {
|
||||
if strings.Contains(err.Error(), "did not match any file(s) known to git") {
|
||||
if err := c.RunCommand("git config --file .gitmodules --remove-section submodule.%s", submodule.Name); err != nil {
|
||||
if err := c.RunCommand("git config --file .gitmodules --remove-section submodule.%s", c.OSCommand.Quote(submodule.Name)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.RunCommand("git config --remove-section submodule.%s", submodule.Name); err != nil {
|
||||
if err := c.RunCommand("git config --remove-section submodule.%s", c.OSCommand.Quote(submodule.Name)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -119,11 +119,11 @@ func (c *GitCommand) SubmoduleAdd(name string, path string, url string) error {
|
||||
|
||||
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.RunCommand("git config --file .gitmodules submodule.%s.url %s", name, newUrl); err != nil {
|
||||
if err := c.RunCommand("git config --file .gitmodules submodule.%s.url %s", c.OSCommand.Quote(name), c.OSCommand.Quote(newUrl)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.RunCommand("git submodule sync %s", path); err != nil {
|
||||
if err := c.RunCommand("git submodule sync -- %s", c.OSCommand.Quote(path)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -131,11 +131,11 @@ func (c *GitCommand) SubmoduleUpdateUrl(name string, path string, newUrl string)
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleInit(path string) error {
|
||||
return c.RunCommand("git submodule init %s", path)
|
||||
return c.RunCommand("git submodule init -- %s", c.OSCommand.Quote(path))
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleUpdate(path string) error {
|
||||
return c.RunCommand("git submodule update --init %s", path)
|
||||
return c.RunCommand("git submodule update --init -- %s", c.OSCommand.Quote(path))
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleBulkInitCmdStr() string {
|
||||
|
||||
+70
-20
@@ -2,27 +2,43 @@ package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
)
|
||||
|
||||
// Push pushes to a branch
|
||||
func (c *GitCommand) Push(branchName string, force bool, upstream string, args string, promptUserForCredential func(string) string) error {
|
||||
followTagsFlag := "--follow-tags"
|
||||
if c.GetConfigValue("push.followTags") == "false" {
|
||||
followTagsFlag = ""
|
||||
type PushOpts struct {
|
||||
Force bool
|
||||
UpstreamRemote string
|
||||
UpstreamBranch string
|
||||
SetUpstream bool
|
||||
PromptUserForCredential func(string) string
|
||||
}
|
||||
|
||||
func (c *GitCommand) Push(opts PushOpts) error {
|
||||
cmdStr := "git push"
|
||||
|
||||
if opts.Force {
|
||||
cmdStr += " --force-with-lease"
|
||||
}
|
||||
|
||||
forceFlag := ""
|
||||
if force {
|
||||
forceFlag = "--force-with-lease"
|
||||
if opts.SetUpstream {
|
||||
cmdStr += " --set-upstream"
|
||||
}
|
||||
|
||||
setUpstreamArg := ""
|
||||
if upstream != "" {
|
||||
setUpstreamArg = "--set-upstream " + upstream
|
||||
if opts.UpstreamRemote != "" {
|
||||
cmdStr += " " + c.OSCommand.Quote(opts.UpstreamRemote)
|
||||
}
|
||||
|
||||
cmd := fmt.Sprintf("git push %s %s %s %s", followTagsFlag, forceFlag, setUpstreamArg, args)
|
||||
return c.OSCommand.DetectUnamePass(cmd, promptUserForCredential)
|
||||
if opts.UpstreamBranch != "" {
|
||||
if opts.UpstreamRemote == "" {
|
||||
return errors.New(c.Tr.MustSpecifyOriginError)
|
||||
}
|
||||
cmdStr += " " + c.OSCommand.Quote(opts.UpstreamBranch)
|
||||
}
|
||||
|
||||
cmdObj := c.NewCmdObjFromStr(cmdStr)
|
||||
return c.DetectUnamePass(cmdObj, opts.PromptUserForCredential)
|
||||
}
|
||||
|
||||
type FetchOptions struct {
|
||||
@@ -33,16 +49,17 @@ type FetchOptions struct {
|
||||
|
||||
// Fetch fetch git repo
|
||||
func (c *GitCommand) Fetch(opts FetchOptions) error {
|
||||
command := "git fetch"
|
||||
cmdStr := "git fetch"
|
||||
|
||||
if opts.RemoteName != "" {
|
||||
command = fmt.Sprintf("%s %s", command, opts.RemoteName)
|
||||
cmdStr = fmt.Sprintf("%s %s", cmdStr, c.OSCommand.Quote(opts.RemoteName))
|
||||
}
|
||||
if opts.BranchName != "" {
|
||||
command = fmt.Sprintf("%s %s", command, opts.BranchName)
|
||||
cmdStr = fmt.Sprintf("%s %s", cmdStr, c.OSCommand.Quote(opts.BranchName))
|
||||
}
|
||||
|
||||
return c.OSCommand.DetectUnamePass(command, func(question string) string {
|
||||
cmdObj := c.NewCmdObjFromStr(cmdStr)
|
||||
return c.DetectUnamePass(cmdObj, func(question string) string {
|
||||
if opts.PromptUserForCredential != nil {
|
||||
return opts.PromptUserForCredential(question)
|
||||
}
|
||||
@@ -50,12 +67,45 @@ func (c *GitCommand) Fetch(opts FetchOptions) error {
|
||||
})
|
||||
}
|
||||
|
||||
type PullOptions struct {
|
||||
PromptUserForCredential func(string) string
|
||||
RemoteName string
|
||||
BranchName string
|
||||
FastForwardOnly bool
|
||||
}
|
||||
|
||||
func (c *GitCommand) Pull(opts PullOptions) error {
|
||||
if opts.PromptUserForCredential == nil {
|
||||
return errors.New("PromptUserForCredential is required")
|
||||
}
|
||||
|
||||
cmdStr := "git pull --no-edit"
|
||||
|
||||
if opts.FastForwardOnly {
|
||||
cmdStr += " --ff-only"
|
||||
}
|
||||
|
||||
if opts.RemoteName != "" {
|
||||
cmdStr = fmt.Sprintf("%s %s", cmdStr, c.OSCommand.Quote(opts.RemoteName))
|
||||
}
|
||||
if opts.BranchName != "" {
|
||||
cmdStr = fmt.Sprintf("%s %s", cmdStr, c.OSCommand.Quote(opts.BranchName))
|
||||
}
|
||||
|
||||
// setting GIT_SEQUENCE_EDITOR to ':' as a way of skipping it, in case the user
|
||||
// has 'pull.rebase = interactive' configured.
|
||||
cmdObj := c.NewCmdObjFromStr(cmdStr).AddEnvVars("GIT_SEQUENCE_EDITOR=:")
|
||||
return c.DetectUnamePass(cmdObj, opts.PromptUserForCredential)
|
||||
}
|
||||
|
||||
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)
|
||||
cmdStr := fmt.Sprintf("git fetch %s %s:%s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(remoteBranchName), c.OSCommand.Quote(branchName))
|
||||
cmdObj := c.NewCmdObjFromStr(cmdStr)
|
||||
return c.DetectUnamePass(cmdObj, 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)
|
||||
cmdStr := fmt.Sprintf("git fetch %s", c.OSCommand.Quote(remoteName))
|
||||
cmdObj := c.NewCmdObjFromStr(cmdStr)
|
||||
return c.DetectUnamePass(cmdObj, promptUserForCredential)
|
||||
}
|
||||
|
||||
+118
-52
@@ -11,87 +11,153 @@ import (
|
||||
// TestGitCommandPush is a function.
|
||||
func TestGitCommandPush(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
getGitConfigValue func(string) (string, error)
|
||||
command func(string, ...string) *exec.Cmd
|
||||
forcePush bool
|
||||
test func(error)
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
opts PushOpts
|
||||
test func(error)
|
||||
}
|
||||
|
||||
prompt := func(passOrUname string) string {
|
||||
return "\n"
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Push with force disabled, follow-tags on",
|
||||
func(string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push", "--follow-tags"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
false,
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with force enabled, follow-tags on",
|
||||
func(string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push", "--follow-tags", "--force-with-lease"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
true,
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with force disabled, follow-tags off",
|
||||
func(string) (string, error) {
|
||||
return "false", nil
|
||||
},
|
||||
"Push with force disabled",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
false,
|
||||
PushOpts{Force: false, PromptUserForCredential: prompt},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with an error occurring, follow-tags on",
|
||||
func(string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
"Push with force enabled",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push", "--follow-tags"}, args)
|
||||
assert.EqualValues(t, []string{"push", "--force-with-lease"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
PushOpts{Force: true, PromptUserForCredential: prompt},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with an error occurring",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push"}, args)
|
||||
return secureexec.Command("test")
|
||||
},
|
||||
false,
|
||||
PushOpts{Force: false, PromptUserForCredential: prompt},
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with force disabled, upstream supplied",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push", "origin", "master"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
PushOpts{
|
||||
Force: false,
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "master",
|
||||
PromptUserForCredential: prompt,
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with force disabled, setting upstream",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push", "--set-upstream", "origin", "master"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
PushOpts{
|
||||
Force: false,
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "master",
|
||||
PromptUserForCredential: prompt,
|
||||
SetUpstream: true,
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with force enabled, setting upstream",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push", "--force-with-lease", "--set-upstream", "origin", "master"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
PushOpts{
|
||||
Force: true,
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "master",
|
||||
PromptUserForCredential: prompt,
|
||||
SetUpstream: true,
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with remote branch but no origin",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return nil
|
||||
},
|
||||
PushOpts{
|
||||
Force: true,
|
||||
UpstreamRemote: "",
|
||||
UpstreamBranch: "master",
|
||||
PromptUserForCredential: prompt,
|
||||
SetUpstream: true,
|
||||
},
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
assert.EqualValues(t, "Must specify a remote if specifying a branch", err.Error())
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with force disabled, upstream supplied",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push", "origin", "master"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
PushOpts{
|
||||
Force: false,
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "master",
|
||||
PromptUserForCredential: prompt,
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
gitCmd.getGitConfigValue = s.getGitConfigValue
|
||||
err := gitCmd.Push("test", s.forcePush, "", "", func(passOrUname string) string {
|
||||
return "\n"
|
||||
})
|
||||
err := gitCmd.Push(s.opts)
|
||||
s.test(err)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
package commands
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func (c *GitCommand) CreateLightweightTag(tagName string, commitSha string) error {
|
||||
return c.RunCommand("git tag %s %s", tagName, commitSha)
|
||||
return c.RunCommand("git tag -- %s %s", c.OSCommand.Quote(tagName), commitSha)
|
||||
}
|
||||
|
||||
func (c *GitCommand) DeleteTag(tagName string) error {
|
||||
return c.RunCommand("git tag -d %s", tagName)
|
||||
return c.RunCommand("git tag -d %s", c.OSCommand.Quote(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)
|
||||
cmdStr := fmt.Sprintf("git push %s %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(tagName))
|
||||
cmdObj := c.NewCmdObjFromStr(cmdStr)
|
||||
return c.DetectUnamePass(cmdObj, promptUserForCredential)
|
||||
}
|
||||
|
||||
+111
-58
@@ -12,17 +12,19 @@ import (
|
||||
|
||||
// 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 *UserConfig
|
||||
UserConfigPaths []string
|
||||
DeafultConfFiles bool
|
||||
UserConfigDir string
|
||||
TempDir string
|
||||
AppState *AppState
|
||||
IsNewRepo bool
|
||||
}
|
||||
|
||||
// AppConfigurer interface allows individual app config structs to inherit Fields
|
||||
@@ -35,23 +37,35 @@ type AppConfigurer interface {
|
||||
GetName() string
|
||||
GetBuildSource() string
|
||||
GetUserConfig() *UserConfig
|
||||
GetUserConfigPaths() []string
|
||||
GetUserConfigDir() string
|
||||
GetUserConfigPath() string
|
||||
GetTempDir() string
|
||||
GetAppState() *AppState
|
||||
SaveAppState() error
|
||||
SetIsNewRepo(bool)
|
||||
GetIsNewRepo() bool
|
||||
ReloadUserConfig() error
|
||||
ShowCommandLogOnStartup() 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 {
|
||||
if err != nil && !os.IsPermission(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userConfig, err := loadUserConfigWithDefaults(configDir)
|
||||
var userConfigPaths []string
|
||||
customConfigFiles := os.Getenv("LG_CONFIG_FILE")
|
||||
if customConfigFiles != "" {
|
||||
// Load user defined config files
|
||||
userConfigPaths = strings.Split(customConfigFiles, ",")
|
||||
} else {
|
||||
// Load default config files
|
||||
userConfigPaths = []string{filepath.Join(configDir, ConfigFilename)}
|
||||
}
|
||||
|
||||
userConfig, err := loadUserConfigWithDefaults(userConfigPaths)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -60,28 +74,35 @@ func NewAppConfig(name, version, commit, date string, buildSource string, debugg
|
||||
debuggingFlag = true
|
||||
}
|
||||
|
||||
tempDir := filepath.Join(os.TempDir(), "lazygit")
|
||||
|
||||
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: configDir,
|
||||
UserConfigPath: filepath.Join(configDir, "config.yml"),
|
||||
AppState: appState,
|
||||
IsNewRepo: false,
|
||||
Name: "lazygit",
|
||||
Version: version,
|
||||
Commit: commit,
|
||||
BuildDate: date,
|
||||
Debug: debuggingFlag,
|
||||
BuildSource: buildSource,
|
||||
UserConfig: userConfig,
|
||||
UserConfigPaths: userConfigPaths,
|
||||
UserConfigDir: configDir,
|
||||
TempDir: tempDir,
|
||||
AppState: appState,
|
||||
IsNewRepo: false,
|
||||
}
|
||||
|
||||
return appConfig, nil
|
||||
}
|
||||
|
||||
func isCustomConfigFile(path string) bool {
|
||||
return path != filepath.Join(ConfigDir(), ConfigFilename)
|
||||
}
|
||||
|
||||
func ConfigDir() string {
|
||||
legacyConfigDirectory := configDirForVendor("jesseduffield")
|
||||
if _, err := os.Stat(legacyConfigDirectory); !os.IsNotExist(err) {
|
||||
@@ -102,43 +123,45 @@ func configDirForVendor(vendor string) string {
|
||||
|
||||
func findOrCreateConfigDir() (string, error) {
|
||||
folder := ConfigDir()
|
||||
err := os.MkdirAll(folder, 0755)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return folder, nil
|
||||
return folder, os.MkdirAll(folder, 0755)
|
||||
}
|
||||
|
||||
func loadUserConfigWithDefaults(configDir string) (*UserConfig, error) {
|
||||
return loadUserConfig(configDir, GetDefaultConfig())
|
||||
func loadUserConfigWithDefaults(configFiles []string) (*UserConfig, error) {
|
||||
return loadUserConfig(configFiles, GetDefaultConfig())
|
||||
}
|
||||
|
||||
func loadUserConfig(configDir string, base *UserConfig) (*UserConfig, error) {
|
||||
fileName := filepath.Join(configDir, "config.yml")
|
||||
func loadUserConfig(configFiles []string, base *UserConfig) (*UserConfig, error) {
|
||||
for _, path := range configFiles {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := os.Stat(fileName); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
file, err := os.Create(fileName)
|
||||
// if use has supplied their own custom config file path(s), we assume
|
||||
// the files have already been created, so we won't go and create them here.
|
||||
if isCustomConfigFile(path) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "read-only file system") {
|
||||
return base, nil
|
||||
if os.IsPermission(err) {
|
||||
// apparently when people have read-only permissions they prefer us to fail silently
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
file.Close()
|
||||
} else {
|
||||
}
|
||||
|
||||
content, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
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
|
||||
if err := yaml.Unmarshal(content, base); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return base, nil
|
||||
@@ -190,22 +213,25 @@ func (c *AppConfig) GetUserConfig() *UserConfig {
|
||||
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
|
||||
}
|
||||
|
||||
func (c *AppConfig) GetUserConfigPaths() []string {
|
||||
return c.UserConfigPaths
|
||||
}
|
||||
|
||||
func (c *AppConfig) GetUserConfigDir() string {
|
||||
return c.UserConfigDir
|
||||
}
|
||||
|
||||
func (c *AppConfig) GetTempDir() string {
|
||||
return c.TempDir
|
||||
}
|
||||
|
||||
func (c *AppConfig) ReloadUserConfig() error {
|
||||
userConfig, err := loadUserConfigWithDefaults(c.UserConfigDir)
|
||||
userConfig, err := loadUserConfigWithDefaults(c.UserConfigPaths)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -223,9 +249,11 @@ func configFilePath(filename string) (string, error) {
|
||||
return filepath.Join(folder, filename), nil
|
||||
}
|
||||
|
||||
// ConfigFilename returns the filename of the current config file
|
||||
var ConfigFilename = "config.yml"
|
||||
|
||||
// ConfigFilename returns the filename of the deafult config file
|
||||
func (c *AppConfig) ConfigFilename() string {
|
||||
return filepath.Join(c.UserConfigDir, "config.yml")
|
||||
return filepath.Join(c.UserConfigDir, ConfigFilename)
|
||||
}
|
||||
|
||||
// SaveAppState marshalls the AppState struct and writes it to the disk
|
||||
@@ -240,13 +268,34 @@ func (c *AppConfig) SaveAppState() error {
|
||||
return err
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(filepath, marshalledAppState, 0644)
|
||||
err = ioutil.WriteFile(filepath, marshalledAppState, 0644)
|
||||
if err != nil && os.IsPermission(err) {
|
||||
// apparently when people have read-only permissions they prefer us to fail silently
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// originally we could only hide the command log permanently via the config
|
||||
// but now we do it via state. So we need to still support the config for the
|
||||
// sake of backwards compatibility
|
||||
func (c *AppConfig) ShowCommandLogOnStartup() bool {
|
||||
if !c.UserConfig.Gui.ShowCommandLog {
|
||||
return false
|
||||
}
|
||||
|
||||
return !c.AppState.HideCommandLog
|
||||
}
|
||||
|
||||
// loadAppState loads recorded AppState from file
|
||||
func loadAppState() (*AppState, error) {
|
||||
filepath, err := configFilePath("state.yml")
|
||||
if err != nil {
|
||||
if os.IsPermission(err) {
|
||||
// apparently when people have read-only permissions they prefer us to fail silently
|
||||
return getDefaultAppState(), nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -274,6 +323,10 @@ type AppState struct {
|
||||
LastUpdateCheck int64
|
||||
RecentRepos []string
|
||||
StartupPopupVersion int
|
||||
|
||||
// these are for custom commands typed in directly, not for custom commands in the lazygit config
|
||||
CustomCommandsHistory []string
|
||||
HideCommandLog bool
|
||||
}
|
||||
|
||||
func getDefaultAppState() *AppState {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build !windows && !linux
|
||||
// +build !windows,!linux
|
||||
|
||||
package config
|
||||
@@ -5,7 +6,9 @@ package config
|
||||
// GetPlatformDefaultConfig gets the defaults for the platform
|
||||
func GetPlatformDefaultConfig() OSConfig {
|
||||
return OSConfig{
|
||||
OpenCommand: "open {{filename}}",
|
||||
OpenLinkCommand: "open {{link}}",
|
||||
EditCommand: ``,
|
||||
EditCommandTemplate: `{{editor}} {{filename}}`,
|
||||
OpenCommand: "open {{filename}}",
|
||||
OpenLinkCommand: "open {{link}}",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,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"`,
|
||||
EditCommand: ``,
|
||||
EditCommandTemplate: `{{editor}} {{filename}}`,
|
||||
OpenCommand: `xdg-open {{filename}} >/dev/null`,
|
||||
OpenLinkCommand: `xdg-open {{link}} >/dev/null`,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,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}}"`,
|
||||
EditCommand: ``,
|
||||
EditCommandTemplate: `{{editor}} {{filename}}`,
|
||||
OpenCommand: `start "" {{filename}}`,
|
||||
OpenLinkCommand: `start "" {{link}}`,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ func NewDummyAppConfig() *AppConfig {
|
||||
Debug: false,
|
||||
BuildSource: "",
|
||||
UserConfig: GetDefaultConfig(),
|
||||
AppState: &AppState{},
|
||||
}
|
||||
_ = yaml.Unmarshal([]byte{}, appConfig.AppState)
|
||||
return appConfig
|
||||
|
||||
+117
-75
@@ -24,6 +24,7 @@ type RefresherConfig struct {
|
||||
}
|
||||
|
||||
type GuiConfig struct {
|
||||
AuthorColors map[string]string `yaml:"authorColors"`
|
||||
ScrollHeight int `yaml:"scrollHeight"`
|
||||
ScrollPastBottom bool `yaml:"scrollPastBottom"`
|
||||
MouseEvents bool `yaml:"mouseEvents"`
|
||||
@@ -32,9 +33,11 @@ type GuiConfig struct {
|
||||
SidePanelWidth float64 `yaml:"sidePanelWidth"`
|
||||
ExpandFocusedSidePanel bool `yaml:"expandFocusedSidePanel"`
|
||||
MainPanelSplitMode string `yaml:"mainPanelSplitMode"`
|
||||
Language string `yaml:"language"`
|
||||
Theme ThemeConfig `yaml:"theme"`
|
||||
CommitLength CommitLengthConfig `yaml:"commitLength"`
|
||||
SkipNoStagedFilesWarning bool `yaml:"skipNoStagedFilesWarning"`
|
||||
ShowListFooter bool `yaml:"showListFooter"`
|
||||
ShowFileTree bool `yaml:"showFileTree"`
|
||||
ShowRandomTip bool `yaml:"showRandomTip"`
|
||||
ShowCommandLog bool `yaml:"showCommandLog"`
|
||||
@@ -42,12 +45,14 @@ type GuiConfig struct {
|
||||
}
|
||||
|
||||
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"`
|
||||
LightTheme bool `yaml:"lightTheme"`
|
||||
ActiveBorderColor []string `yaml:"activeBorderColor"`
|
||||
InactiveBorderColor []string `yaml:"inactiveBorderColor"`
|
||||
OptionsTextColor []string `yaml:"optionsTextColor"`
|
||||
SelectedLineBgColor []string `yaml:"selectedLineBgColor"`
|
||||
SelectedRangeBgColor []string `yaml:"selectedRangeBgColor"`
|
||||
CherryPickedCommitBgColor []string `yaml:"cherryPickedCommitBgColor"`
|
||||
CherryPickedCommitFgColor []string `yaml:"cherryPickedCommitFgColor"`
|
||||
}
|
||||
|
||||
type CommitLengthConfig struct {
|
||||
@@ -57,7 +62,6 @@ type CommitLengthConfig struct {
|
||||
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"`
|
||||
@@ -65,6 +69,9 @@ type GitConfig struct {
|
||||
OverrideGpg bool `yaml:"overrideGpg"`
|
||||
DisableForcePushing bool `yaml:"disableForcePushing"`
|
||||
CommitPrefixes map[string]CommitPrefixConfig `yaml:"commitPrefixes"`
|
||||
// this shoudl really be under 'gui', not 'git'
|
||||
ParseEmoji bool `yaml:"parseEmoji"`
|
||||
Log LogConfig `yaml:"log"`
|
||||
}
|
||||
|
||||
type PagingConfig struct {
|
||||
@@ -78,8 +85,9 @@ type MergingConfig struct {
|
||||
Args string `yaml:"args"`
|
||||
}
|
||||
|
||||
type PullConfig struct {
|
||||
Mode string `yaml:"mode"`
|
||||
type LogConfig struct {
|
||||
Order string `yaml:"order"` // one of date-order, author-date-order, topo-order
|
||||
ShowGraph string `yaml:"showGraph"` // one of always, never, when-maximised
|
||||
}
|
||||
|
||||
type CommitPrefixConfig struct {
|
||||
@@ -106,63 +114,68 @@ type KeybindingConfig struct {
|
||||
|
||||
// damn looks like we have some inconsistencies here with -alt and -alt1
|
||||
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"`
|
||||
NextBlockAlt2 string `yaml:"nextBlock-alt2"`
|
||||
PrevBlockAlt2 string `yaml:"prevBlock-alt2"`
|
||||
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"`
|
||||
ExtrasMenu string `yaml:"extrasMenu"`
|
||||
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"`
|
||||
ScrollLeft string `yaml:"scrollLeft"`
|
||||
ScrollRight string `yaml:"scrollRight"`
|
||||
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"`
|
||||
NextBlockAlt2 string `yaml:"nextBlock-alt2"`
|
||||
PrevBlockAlt2 string `yaml:"prevBlock-alt2"`
|
||||
JumpToBlock []string `yaml:"jumpToBlock"`
|
||||
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"`
|
||||
OpenRecentRepos string `yaml:"openRecentRepos"`
|
||||
SubmitEditorText string `yaml:"submitEditorText"`
|
||||
AppendNewline string `yaml:"appendNewline"`
|
||||
ExtrasMenu string `yaml:"extrasMenu"`
|
||||
ToggleWhitespaceInDiffView string `yaml:"toggleWhitespaceInDiffView"`
|
||||
}
|
||||
|
||||
type KeybindingStatusConfig struct {
|
||||
@@ -185,10 +198,12 @@ type KeybindingFilesConfig struct {
|
||||
Fetch string `yaml:"fetch"`
|
||||
ToggleTreeView string `yaml:"toggleTreeView"`
|
||||
OpenMergeTool string `yaml:"openMergeTool"`
|
||||
OpenStatusFilter string `yaml:"openStatusFilter"`
|
||||
}
|
||||
|
||||
type KeybindingBranchesConfig struct {
|
||||
CreatePullRequest string `yaml:"createPullRequest"`
|
||||
ViewPullRequestOptions string `yaml:"viewPullRequestOptions"`
|
||||
CopyPullRequestURL string `yaml:"copyPullRequestURL"`
|
||||
CheckoutBranchByName string `yaml:"checkoutBranchByName"`
|
||||
ForceCheckoutBranch string `yaml:"forceCheckoutBranch"`
|
||||
@@ -222,6 +237,7 @@ type KeybindingCommitsConfig struct {
|
||||
CheckoutCommit string `yaml:"checkoutCommit"`
|
||||
ResetCherryPick string `yaml:"resetCherryPick"`
|
||||
CopyCommitMessageToClipboard string `yaml:"copyCommitMessageToClipboard"`
|
||||
OpenLogMenu string `yaml:"openLogMenu"`
|
||||
}
|
||||
|
||||
type KeybindingStashConfig struct {
|
||||
@@ -247,6 +263,12 @@ type KeybindingSubmodulesConfig struct {
|
||||
|
||||
// OSConfig contains config on the level of the os
|
||||
type OSConfig struct {
|
||||
// EditCommand is the command for editing a file
|
||||
EditCommand string `yaml:"editCommand,omitempty"`
|
||||
|
||||
// EditCommandTemplate is the command template for editing a file
|
||||
EditCommandTemplate string `yaml:"editCommandTemplate,omitempty"`
|
||||
|
||||
// OpenCommand is the command for opening a file
|
||||
OpenCommand string `yaml:"openCommand,omitempty"`
|
||||
|
||||
@@ -273,6 +295,12 @@ type CustomCommandPrompt struct {
|
||||
|
||||
// this only applies to menus
|
||||
Options []CustomCommandMenuOption
|
||||
|
||||
// this only applies to menuFromCommand
|
||||
Command string `yaml:"command"`
|
||||
Filter string `yaml:"filter"`
|
||||
ValueFormat string `yaml:"valueFormat"`
|
||||
LabelFormat string `yaml:"labelFormat"`
|
||||
}
|
||||
|
||||
type CustomCommandMenuOption struct {
|
||||
@@ -292,18 +320,22 @@ func GetDefaultConfig() *UserConfig {
|
||||
SidePanelWidth: 0.3333,
|
||||
ExpandFocusedSidePanel: false,
|
||||
MainPanelSplitMode: "flexible",
|
||||
Language: "auto",
|
||||
Theme: ThemeConfig{
|
||||
LightTheme: false,
|
||||
ActiveBorderColor: []string{"green", "bold"},
|
||||
InactiveBorderColor: []string{"white"},
|
||||
OptionsTextColor: []string{"blue"},
|
||||
SelectedLineBgColor: []string{"default"},
|
||||
SelectedRangeBgColor: []string{"blue"},
|
||||
LightTheme: false,
|
||||
ActiveBorderColor: []string{"green", "bold"},
|
||||
InactiveBorderColor: []string{"white"},
|
||||
OptionsTextColor: []string{"blue"},
|
||||
SelectedLineBgColor: []string{"default"},
|
||||
SelectedRangeBgColor: []string{"blue"},
|
||||
CherryPickedCommitBgColor: []string{"blue"},
|
||||
CherryPickedCommitFgColor: []string{"cyan"},
|
||||
},
|
||||
CommitLength: CommitLengthConfig{Show: true},
|
||||
SkipNoStagedFilesWarning: false,
|
||||
ShowListFooter: true,
|
||||
ShowCommandLog: true,
|
||||
ShowFileTree: false,
|
||||
ShowFileTree: true,
|
||||
ShowRandomTip: true,
|
||||
CommandLogSize: 8,
|
||||
},
|
||||
@@ -316,8 +348,9 @@ func GetDefaultConfig() *UserConfig {
|
||||
ManualCommit: false,
|
||||
Args: "",
|
||||
},
|
||||
Pull: PullConfig{
|
||||
Mode: "merge",
|
||||
Log: LogConfig{
|
||||
Order: "topo-order",
|
||||
ShowGraph: "when-maximised",
|
||||
},
|
||||
SkipHookPrefix: "WIP",
|
||||
AutoFetch: true,
|
||||
@@ -325,6 +358,7 @@ func GetDefaultConfig() *UserConfig {
|
||||
AllBranchesLogCmd: "git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium",
|
||||
DisableForcePushing: false,
|
||||
CommitPrefixes: map[string]CommitPrefixConfig(nil),
|
||||
ParseEmoji: false,
|
||||
},
|
||||
Refresher: RefresherConfig{
|
||||
RefreshInterval: 10,
|
||||
@@ -351,6 +385,8 @@ func GetDefaultConfig() *UserConfig {
|
||||
NextItemAlt: "j",
|
||||
PrevPage: ",",
|
||||
NextPage: ".",
|
||||
ScrollLeft: "H",
|
||||
ScrollRight: "L",
|
||||
GotoTop: "<",
|
||||
GotoBottom: ">",
|
||||
PrevBlock: "<left>",
|
||||
@@ -359,6 +395,7 @@ func GetDefaultConfig() *UserConfig {
|
||||
NextBlockAlt: "l",
|
||||
PrevBlockAlt2: "<backtab>",
|
||||
NextBlockAlt2: "<tab>",
|
||||
JumpToBlock: []string{"1", "2", "3", "4", "5"},
|
||||
NextMatch: "n",
|
||||
PrevMatch: "N",
|
||||
StartSearch: "/",
|
||||
@@ -372,6 +409,7 @@ func GetDefaultConfig() *UserConfig {
|
||||
New: "n",
|
||||
Edit: "e",
|
||||
OpenFile: "o",
|
||||
OpenRecentRepos: "<c-r>",
|
||||
ScrollUpMain: "<pgup>",
|
||||
ScrollDownMain: "<pgdown>",
|
||||
ScrollUpMainAlt1: "K",
|
||||
@@ -397,6 +435,7 @@ func GetDefaultConfig() *UserConfig {
|
||||
SubmitEditorText: "<enter>",
|
||||
AppendNewline: "<a-enter>",
|
||||
ExtrasMenu: "@",
|
||||
ToggleWhitespaceInDiffView: "<c-w>",
|
||||
},
|
||||
Status: KeybindingStatusConfig{
|
||||
CheckForUpdate: "u",
|
||||
@@ -417,10 +456,12 @@ func GetDefaultConfig() *UserConfig {
|
||||
Fetch: "f",
|
||||
ToggleTreeView: "`",
|
||||
OpenMergeTool: "M",
|
||||
OpenStatusFilter: "<c-b>",
|
||||
},
|
||||
Branches: KeybindingBranchesConfig{
|
||||
CopyPullRequestURL: "<c-y>",
|
||||
CreatePullRequest: "o",
|
||||
ViewPullRequestOptions: "O",
|
||||
CheckoutBranchByName: "c",
|
||||
ForceCheckoutBranch: "F",
|
||||
RebaseBranch: "r",
|
||||
@@ -452,6 +493,7 @@ func GetDefaultConfig() *UserConfig {
|
||||
CheckoutCommit: "<space>",
|
||||
ResetCherryPick: "<c-R>",
|
||||
CopyCommitMessageToClipboard: "<c-y>",
|
||||
OpenLogMenu: "<c-l>",
|
||||
},
|
||||
Stash: KeybindingStashConfig{
|
||||
PopStash: "g",
|
||||
|
||||
@@ -210,7 +210,7 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
|
||||
|
||||
// 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
|
||||
// the default behaviour when accordion 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 {
|
||||
gui.State.ContextManager.RLock()
|
||||
@@ -259,9 +259,9 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
|
||||
fullHeightBox("stash"),
|
||||
}
|
||||
} else if height >= 28 {
|
||||
accordianMode := gui.Config.GetUserConfig().Gui.ExpandFocusedSidePanel
|
||||
accordianBox := func(defaultBox *boxlayout.Box) *boxlayout.Box {
|
||||
if accordianMode && defaultBox.Window == currentWindow {
|
||||
accordionMode := gui.Config.GetUserConfig().Gui.ExpandFocusedSidePanel
|
||||
accordionBox := func(defaultBox *boxlayout.Box) *boxlayout.Box {
|
||||
if accordionMode && defaultBox.Window == currentWindow {
|
||||
return &boxlayout.Box{
|
||||
Window: defaultBox.Window,
|
||||
Weight: 2,
|
||||
@@ -276,10 +276,10 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*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()),
|
||||
accordionBox(&boxlayout.Box{Window: "files", Weight: 1}),
|
||||
accordionBox(&boxlayout.Box{Window: "branches", Weight: 1}),
|
||||
accordionBox(&boxlayout.Box{Window: "commits", Weight: 1}),
|
||||
accordionBox(gui.getDefaultStashWindowBox()),
|
||||
}
|
||||
} else {
|
||||
squashedHeight := 1
|
||||
|
||||
+22
-51
@@ -7,8 +7,6 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -91,23 +89,25 @@ func (gui *Gui) handleBranchPress() error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreatePullRequestPress() error {
|
||||
pullRequest := commands.NewPullRequest(gui.GitCommand)
|
||||
|
||||
branch := gui.getSelectedBranch()
|
||||
url, err := pullRequest.Create(branch)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(fmt.Sprintf("Creating pull request at URL: %s", url), "Create pull request", false))
|
||||
return gui.createPullRequest(branch.Name, "")
|
||||
}
|
||||
|
||||
return nil
|
||||
func (gui *Gui) handleCreatePullRequestMenu() error {
|
||||
selectedBranch := gui.getSelectedBranch()
|
||||
if selectedBranch == nil {
|
||||
return nil
|
||||
}
|
||||
checkedOutBranch := gui.getCheckedOutBranch()
|
||||
|
||||
return gui.createPullRequestMenu(selectedBranch, checkedOutBranch)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCopyPullRequestURLPress() error {
|
||||
pullRequest := commands.NewPullRequest(gui.GitCommand)
|
||||
|
||||
branch := gui.getSelectedBranch()
|
||||
url, err := pullRequest.CopyURL(branch)
|
||||
url, err := pullRequest.CopyURL(branch.Name, "")
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
@@ -218,7 +218,7 @@ func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions)
|
||||
func (gui *Gui) handleCheckoutByName() error {
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.BranchName + ":",
|
||||
findSuggestionsFunc: gui.findBranchNameSuggestions,
|
||||
findSuggestionsFunc: gui.getRefsSuggestionsFunc(),
|
||||
handleConfirm: func(response string) error {
|
||||
return gui.handleCheckoutRef(response, handleCheckoutRefOptions{
|
||||
span: "Checkout branch",
|
||||
@@ -296,7 +296,7 @@ func (gui *Gui) deleteNamedBranch(selectedBranch *models.Branch, force bool) err
|
||||
handleConfirm: func() error {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.DeleteBranch).DeleteBranch(selectedBranch.Name, force); err != nil {
|
||||
errMessage := err.Error()
|
||||
if !force && strings.Contains(errMessage, "is not fully merged") {
|
||||
if !force && strings.Contains(errMessage, "git branch -D ") {
|
||||
return gui.deleteNamedBranch(selectedBranch, true)
|
||||
}
|
||||
return gui.createErrorPanel(errMessage)
|
||||
@@ -379,16 +379,14 @@ func (gui *Gui) handleRebaseOntoBranch(selectedBranchName string) error {
|
||||
|
||||
func (gui *Gui) handleFastForward() error {
|
||||
branch := gui.getSelectedBranch()
|
||||
if branch == nil {
|
||||
if branch == nil || !branch.IsRealBranch() {
|
||||
return nil
|
||||
}
|
||||
if branch.Pushables == "" {
|
||||
return nil
|
||||
}
|
||||
if branch.Pushables == "?" {
|
||||
|
||||
if !branch.IsTrackingRemote() {
|
||||
return gui.createErrorPanel(gui.Tr.FwdNoUpstream)
|
||||
}
|
||||
if branch.Pushables != "0" {
|
||||
if branch.HasCommitsToPush() {
|
||||
return gui.createErrorPanel(gui.Tr.FwdCommitsToPush)
|
||||
}
|
||||
|
||||
@@ -414,7 +412,7 @@ func (gui *Gui) handleFastForward() error {
|
||||
_ = gui.createLoaderPanel(message)
|
||||
|
||||
if gui.State.Panels.Branches.SelectedLineIdx == 0 {
|
||||
_ = gui.pullWithMode("ff-only", PullFilesOptions{span: span})
|
||||
_ = gui.pullWithLock(PullFilesOptions{span: span, FastForwardOnly: true})
|
||||
} else {
|
||||
err := gui.GitCommand.WithSpan(span).FastForward(branch.Name, remoteName, remoteBranchName, gui.promptUserForCredential)
|
||||
gui.handleCredentialsPopup(err)
|
||||
@@ -435,7 +433,7 @@ func (gui *Gui) handleCreateResetToBranchMenu() error {
|
||||
|
||||
func (gui *Gui) handleRenameBranch() error {
|
||||
branch := gui.getSelectedBranch()
|
||||
if branch == nil {
|
||||
if branch == nil || !branch.IsRealBranch() {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -469,8 +467,7 @@ func (gui *Gui) handleRenameBranch() error {
|
||||
// I could do an explicit check here for whether the branch is tracking a remote branch
|
||||
// but if we've selected it we'll already know that via Pullables and Pullables.
|
||||
// Bit of a hack but I'm lazy.
|
||||
notTrackingRemote := branch.Pullables == "?"
|
||||
if notTrackingRemote {
|
||||
if !branch.IsTrackingRemote() {
|
||||
return promptForNewName()
|
||||
}
|
||||
|
||||
@@ -505,8 +502,8 @@ func (gui *Gui) handleNewBranchOffCurrentItem() error {
|
||||
|
||||
prefilledName := ""
|
||||
if context.GetKey() == REMOTE_BRANCHES_CONTEXT_KEY {
|
||||
// will set to the remote's existing name
|
||||
prefilledName = item.ID()
|
||||
// will set to the remote's branch name without the remote name
|
||||
prefilledName = strings.SplitAfterN(item.ID(), "/", 2)[1]
|
||||
}
|
||||
|
||||
return gui.prompt(promptOpts{
|
||||
@@ -536,32 +533,6 @@ func (gui *Gui) handleNewBranchOffCurrentItem() error {
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) getBranchNames() []string {
|
||||
result := make([]string, len(gui.State.Branches))
|
||||
|
||||
for i, branch := range gui.State.Branches {
|
||||
result[i] = branch.Name
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (gui *Gui) findBranchNameSuggestions(input string) []*types.Suggestion {
|
||||
branchNames := gui.getBranchNames()
|
||||
|
||||
matchingBranchNames := utils.FuzzySearch(sanitizedBranchName(input), branchNames)
|
||||
|
||||
suggestions := make([]*types.Suggestion, len(matchingBranchNames))
|
||||
for i, branchName := range matchingBranchNames {
|
||||
suggestions[i] = &types.Suggestion{
|
||||
Value: branchName,
|
||||
Label: utils.ColoredString(branchName, presentation.GetBranchColor(branchName)),
|
||||
}
|
||||
}
|
||||
|
||||
return suggestions
|
||||
}
|
||||
|
||||
// sanitizedBranchName will remove all spaces in favor of a dash "-" to meet
|
||||
// git's branch naming requirement.
|
||||
func sanitizedBranchName(input string) string {
|
||||
|
||||
@@ -5,11 +5,11 @@ import "github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
// you can only copy from one context at a time, because the order and position of commits matter
|
||||
|
||||
func (gui *Gui) resetCherryPickingIfNecessary(context Context) error {
|
||||
oldContextKey := gui.State.Modes.CherryPicking.ContextKey
|
||||
oldContextKey := ContextKey(gui.State.Modes.CherryPicking.ContextKey)
|
||||
|
||||
if oldContextKey != context.GetKey() {
|
||||
// need to reset the cherry picking mode
|
||||
gui.State.Modes.CherryPicking.ContextKey = context.GetKey()
|
||||
gui.State.Modes.CherryPicking.ContextKey = string(context.GetKey())
|
||||
gui.State.Modes.CherryPicking.CherryPickedCommits = make([]*models.Commit, 0)
|
||||
|
||||
return gui.rerenderContextViewIfPresent(oldContextKey)
|
||||
@@ -33,7 +33,7 @@ func (gui *Gui) handleCopyCommit() error {
|
||||
return err
|
||||
}
|
||||
|
||||
item, ok := context.SelectedItem()
|
||||
item, ok := context.GetSelectedItem()
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
@@ -156,7 +156,7 @@ func (gui *Gui) HandlePasteCommits() error {
|
||||
}
|
||||
|
||||
func (gui *Gui) exitCherryPickingMode() error {
|
||||
contextKey := gui.State.Modes.CherryPicking.ContextKey
|
||||
contextKey := ContextKey(gui.State.Modes.CherryPicking.ContextKey)
|
||||
|
||||
gui.State.Modes.CherryPicking.ContextKey = ""
|
||||
gui.State.Modes.CherryPicking.CherryPickedCommits = nil
|
||||
|
||||
@@ -6,11 +6,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/constants"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func (gui *Gui) GetOnRunCommand() func(entry oscommands.CmdLogEntry) {
|
||||
@@ -25,17 +24,17 @@ func (gui *Gui) GetOnRunCommand() func(entry oscommands.CmdLogEntry) {
|
||||
gui.Views.Extras.Autoscroll = true
|
||||
|
||||
if entry.GetSpan() != currentSpan {
|
||||
fmt.Fprint(gui.Views.Extras, "\n"+utils.ColoredString(entry.GetSpan(), color.FgYellow))
|
||||
fmt.Fprint(gui.Views.Extras, "\n"+style.FgYellow.Sprint(entry.GetSpan()))
|
||||
currentSpan = entry.GetSpan()
|
||||
}
|
||||
|
||||
clrAttr := theme.DefaultTextColor
|
||||
textStyle := theme.DefaultTextColor
|
||||
if !entry.GetCommandLine() {
|
||||
clrAttr = color.FgMagenta
|
||||
textStyle = style.FgMagenta
|
||||
}
|
||||
gui.CmdLog = append(gui.CmdLog, entry.GetCmdStr())
|
||||
indentedCmdStr := " " + strings.Replace(entry.GetCmdStr(), "\n", "\n ", -1)
|
||||
fmt.Fprint(gui.Views.Extras, "\n"+utils.ColoredString(indentedCmdStr, clrAttr))
|
||||
fmt.Fprint(gui.Views.Extras, "\n"+textStyle.Sprint(indentedCmdStr))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,14 +43,14 @@ func (gui *Gui) printCommandLogHeader() {
|
||||
gui.Tr.CommandLogHeader,
|
||||
gui.getKeyDisplay(gui.Config.GetUserConfig().Keybinding.Universal.ExtrasMenu),
|
||||
)
|
||||
fmt.Fprintln(gui.Views.Extras, utils.ColoredString(introStr, color.FgCyan))
|
||||
fmt.Fprintln(gui.Views.Extras, style.FgCyan.Sprint(introStr))
|
||||
|
||||
if gui.Config.GetUserConfig().Gui.ShowRandomTip {
|
||||
fmt.Fprintf(
|
||||
gui.Views.Extras,
|
||||
"%s: %s",
|
||||
utils.ColoredString(gui.Tr.RandomTip, color.FgYellow),
|
||||
utils.ColoredString(gui.getRandomTip(), color.FgGreen),
|
||||
style.FgYellow.Sprint(gui.Tr.RandomTip),
|
||||
style.FgGreen.Sprint(gui.getRandomTip()),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -102,7 +101,7 @@ func (gui *Gui) getRandomTip() string {
|
||||
formattedKey(config.Universal.GoInto),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"You can diff two commits by pressing '%s' one one commit and then navigating to the other. You can then press '%s' to view the files of the diff",
|
||||
"You can diff two commits by pressing '%s' on one commit and then navigating to the other. You can then press '%s' to view the files of the diff",
|
||||
formattedKey(config.Universal.DiffingMenu),
|
||||
formattedKey(config.Universal.GoInto),
|
||||
),
|
||||
@@ -174,7 +173,7 @@ func (gui *Gui) getRandomTip() string {
|
||||
constants.Links.Docs.CustomCommands,
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"If you ever find a bug, do not hesistate to raise an issue on the repo:\n%s",
|
||||
"If you ever find a bug, do not hesitate to raise an issue on the repo:\n%s",
|
||||
constants.Links.Issues,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func (gui *Gui) handleCommitConfirm() error {
|
||||
message := gui.trimmedContent(gui.Views.CommitMessage)
|
||||
message := strings.TrimSpace(gui.Views.CommitMessage.TextArea.GetContent())
|
||||
if message == "" {
|
||||
return gui.createErrorPanel(gui.Tr.CommitWithoutMessageErr)
|
||||
}
|
||||
@@ -22,9 +22,9 @@ func (gui *Gui) handleCommitConfirm() error {
|
||||
|
||||
cmdStr := gui.GitCommand.CommitCmdStr(message, flags)
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(cmdStr, gui.Tr.Spans.Commit, true))
|
||||
_ = gui.returnFromContext()
|
||||
return gui.withGpgHandling(cmdStr, gui.Tr.CommittingStatus, func() error {
|
||||
_ = gui.returnFromContext()
|
||||
gui.clearEditorView(gui.Views.CommitMessage)
|
||||
gui.Views.CommitMessage.ClearTextArea()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -48,7 +48,7 @@ func (gui *Gui) handleCommitMessageFocused() error {
|
||||
}
|
||||
|
||||
func (gui *Gui) getBufferLength(view *gocui.View) string {
|
||||
return " " + strconv.Itoa(strings.Count(view.Buffer(), "")-1) + " "
|
||||
return " " + strconv.Itoa(strings.Count(view.TextArea.GetContent(), "")-1) + " "
|
||||
}
|
||||
|
||||
// RenderCommitLength is a function.
|
||||
|
||||
+126
-4
@@ -10,6 +10,9 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// after selecting the 200th commit, we'll load in all the rest
|
||||
const COMMIT_THRESHOLD = 200
|
||||
|
||||
// list panel functions
|
||||
|
||||
func (gui *Gui) getSelectedLocalCommit() *models.Commit {
|
||||
@@ -23,7 +26,7 @@ func (gui *Gui) getSelectedLocalCommit() *models.Commit {
|
||||
|
||||
func (gui *Gui) handleCommitSelect() error {
|
||||
state := gui.State.Panels.Commits
|
||||
if state.SelectedLineIdx > 290 && state.LimitCommits {
|
||||
if state.SelectedLineIdx > COMMIT_THRESHOLD && state.LimitCommits {
|
||||
state.LimitCommits = false
|
||||
go utils.Safe(func() {
|
||||
if err := gui.refreshCommitsWithLimit(); err != nil {
|
||||
@@ -122,6 +125,7 @@ func (gui *Gui) refreshCommitsWithLimit() error {
|
||||
FilterPath: gui.State.Modes.Filtering.GetPath(),
|
||||
IncludeRebaseCommits: true,
|
||||
RefName: "HEAD",
|
||||
All: gui.State.ShowWholeGitGraph,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@@ -461,9 +465,43 @@ func (gui *Gui) handleCommitRevert() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.RevertCommit).Revert(gui.State.Commits[gui.State.Panels.Commits.SelectedLineIdx].Sha); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
commit := gui.getSelectedLocalCommit()
|
||||
|
||||
if commit.IsMerge() {
|
||||
return gui.createRevertMergeCommitMenu(commit)
|
||||
} else {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.RevertCommit).Revert(commit.Sha); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.afterRevertCommit()
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) createRevertMergeCommitMenu(commit *models.Commit) error {
|
||||
menuItems := make([]*menuItem, len(commit.Parents))
|
||||
for i, parentSha := range commit.Parents {
|
||||
i := i
|
||||
message, err := gui.GitCommand.GetCommitMessageFirstLine(parentSha)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
menuItems[i] = &menuItem{
|
||||
displayString: fmt.Sprintf("%s: %s", utils.SafeTruncate(parentSha, 8), message),
|
||||
onPress: func() error {
|
||||
parentNumber := i + 1
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.RevertCommit).RevertMerge(commit.Sha, parentNumber); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.afterRevertCommit()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return gui.createMenu(gui.Tr.SelectParentCommitForMerge, menuItems, createMenuOptions{showCancel: true})
|
||||
}
|
||||
|
||||
func (gui *Gui) afterRevertCommit() error {
|
||||
gui.State.Panels.Commits.SelectedLineIdx++
|
||||
return gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI, scope: []RefreshableView{COMMITS, BRANCHES}})
|
||||
}
|
||||
@@ -606,7 +644,7 @@ func (gui *Gui) handleGotoBottomForCommitsPanel() error {
|
||||
}
|
||||
|
||||
for _, context := range gui.getListContexts() {
|
||||
if context.ViewName == "commits" {
|
||||
if context.GetViewName() == "commits" {
|
||||
return context.handleGotoBottom()
|
||||
}
|
||||
}
|
||||
@@ -633,3 +671,87 @@ func (gui *Gui) handleCopySelectedCommitMessageToClipboard() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleOpenLogMenu() error {
|
||||
return gui.createMenu(gui.Tr.LogMenuTitle, []*menuItem{
|
||||
{
|
||||
displayString: gui.Tr.ToggleShowGitGraphAll,
|
||||
onPress: func() error {
|
||||
gui.State.ShowWholeGitGraph = !gui.State.ShowWholeGitGraph
|
||||
|
||||
if gui.State.ShowWholeGitGraph {
|
||||
gui.State.Panels.Commits.LimitCommits = false
|
||||
}
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.LcLoadingCommits, func() error {
|
||||
return gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []RefreshableView{COMMITS}})
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: gui.Tr.ShowGitGraph,
|
||||
opensMenu: true,
|
||||
onPress: func() error {
|
||||
onSelect := func(value string) {
|
||||
gui.Config.GetUserConfig().Git.Log.ShowGraph = value
|
||||
gui.render()
|
||||
}
|
||||
return gui.createMenu(gui.Tr.LogMenuTitle, []*menuItem{
|
||||
{
|
||||
displayString: "always",
|
||||
onPress: func() error {
|
||||
onSelect("always")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: "never",
|
||||
onPress: func() error {
|
||||
onSelect("never")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: "when maximised",
|
||||
onPress: func() error {
|
||||
onSelect("when-maximised")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}, createMenuOptions{showCancel: true})
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: gui.Tr.SortCommits,
|
||||
opensMenu: true,
|
||||
onPress: func() error {
|
||||
onSelect := func(value string) error {
|
||||
gui.Config.GetUserConfig().Git.Log.Order = value
|
||||
return gui.WithWaitingStatus(gui.Tr.LcLoadingCommits, func() error {
|
||||
return gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []RefreshableView{COMMITS}})
|
||||
})
|
||||
}
|
||||
return gui.createMenu(gui.Tr.LogMenuTitle, []*menuItem{
|
||||
{
|
||||
displayString: "topological (topo-order)",
|
||||
onPress: func() error {
|
||||
return onSelect("topo-order")
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: "date-order",
|
||||
onPress: func() error {
|
||||
return onSelect("date-order")
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: "author-date-order",
|
||||
onPress: func() error {
|
||||
return onSelect("author-date-order")
|
||||
},
|
||||
},
|
||||
}, createMenuOptions{showCancel: true})
|
||||
},
|
||||
},
|
||||
}, createMenuOptions{showCancel: true})
|
||||
}
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
// lots of this has been directly ported from one of the example files, will brush up later
|
||||
|
||||
// Copyright 2014 The gocui Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
@@ -37,7 +34,6 @@ type askOpts struct {
|
||||
handleConfirm func() error
|
||||
handleClose func() error
|
||||
handlersManageFocus bool
|
||||
findSuggestionsFunc func(string) []*types.Suggestion
|
||||
}
|
||||
|
||||
type promptOpts struct {
|
||||
@@ -54,7 +50,6 @@ func (gui *Gui) ask(opts askOpts) error {
|
||||
handleConfirm: opts.handleConfirm,
|
||||
handleClose: opts.handleClose,
|
||||
handlersManageFocus: opts.handlersManageFocus,
|
||||
findSuggestionsFunc: opts.findSuggestionsFunc,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -107,13 +102,6 @@ func (gui *Gui) wrappedPromptConfirmationFunction(handlersManageFocus bool, func
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) clearConfirmationViewKeyBindings() {
|
||||
keybindingConfig := gui.Config.GetUserConfig().Keybinding
|
||||
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.Confirm), gocui.ModNone)
|
||||
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone)
|
||||
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.Return), gocui.ModNone)
|
||||
}
|
||||
|
||||
func (gui *Gui) closeConfirmationPrompt(handlersManageFocus bool) error {
|
||||
// we've already closed it so we can just return
|
||||
if !gui.Views.Confirmation.Visible {
|
||||
@@ -169,7 +157,13 @@ func (gui *Gui) getConfirmationPanelDimensions(wrap bool, prompt string) (int, i
|
||||
height/2 + panelHeight/2
|
||||
}
|
||||
|
||||
func (gui *Gui) prepareConfirmationPanel(title, prompt string, hasLoader bool, findSuggestionsFunc func(string) []*types.Suggestion) error {
|
||||
func (gui *Gui) prepareConfirmationPanel(
|
||||
title,
|
||||
prompt string,
|
||||
hasLoader bool,
|
||||
findSuggestionsFunc func(string) []*types.Suggestion,
|
||||
editable bool,
|
||||
) error {
|
||||
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(true, prompt)
|
||||
// calling SetView on an existing view returns the same view, so I'm not bothering
|
||||
// to reassign to gui.Views.Confirmation
|
||||
@@ -182,20 +176,22 @@ func (gui *Gui) prepareConfirmationPanel(title, prompt string, hasLoader bool, f
|
||||
gui.g.StartTicking()
|
||||
}
|
||||
gui.Views.Confirmation.Title = title
|
||||
gui.Views.Confirmation.Wrap = true
|
||||
// for now we do not support wrapping in our editor
|
||||
gui.Views.Confirmation.Wrap = !editable
|
||||
gui.Views.Confirmation.FgColor = theme.GocuiDefaultTextColor
|
||||
|
||||
gui.findSuggestions = findSuggestionsFunc
|
||||
if findSuggestionsFunc != nil {
|
||||
suggestionsViewHeight := 11
|
||||
suggestionsView, err := gui.g.SetView("suggestions", x0, y1, x1, y1+suggestionsViewHeight, 0)
|
||||
suggestionsView, err := gui.g.SetView("suggestions", x0, y1+1, x1, y1+suggestionsViewHeight, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
suggestionsView.Wrap = true
|
||||
suggestionsView.Wrap = false
|
||||
suggestionsView.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.setSuggestions([]*types.Suggestion{})
|
||||
gui.setSuggestions(findSuggestionsFunc(""))
|
||||
suggestionsView.Visible = true
|
||||
suggestionsView.Title = fmt.Sprintf(gui.Tr.SuggestionsTitle, gui.Config.GetUserConfig().Keybinding.Universal.TogglePanel)
|
||||
}
|
||||
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
@@ -209,19 +205,27 @@ func (gui *Gui) createPopupPanel(opts createPopupPanelOpts) error {
|
||||
// remove any previous keybindings
|
||||
gui.clearConfirmationViewKeyBindings()
|
||||
|
||||
err := gui.prepareConfirmationPanel(opts.title, opts.prompt, opts.hasLoader, opts.findSuggestionsFunc)
|
||||
err := gui.prepareConfirmationPanel(
|
||||
opts.title,
|
||||
opts.prompt,
|
||||
opts.hasLoader,
|
||||
opts.findSuggestionsFunc,
|
||||
opts.editable,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gui.Views.Confirmation.Editable = opts.editable
|
||||
gui.Views.Confirmation.Editor = gocui.EditorFunc(gui.defaultEditor)
|
||||
confirmationView := gui.Views.Confirmation
|
||||
confirmationView.Editable = opts.editable
|
||||
confirmationView.Editor = gocui.EditorFunc(gui.defaultEditor)
|
||||
|
||||
if opts.editable {
|
||||
if err := gui.Views.Confirmation.SetEditorContent(opts.prompt); err != nil {
|
||||
return err
|
||||
}
|
||||
textArea := confirmationView.TextArea
|
||||
textArea.Clear()
|
||||
textArea.TypeString(opts.prompt)
|
||||
confirmationView.RenderTextArea()
|
||||
} else {
|
||||
if err := gui.renderStringSync(gui.Views.Confirmation, opts.prompt); err != nil {
|
||||
if err := gui.renderStringSync(confirmationView, opts.prompt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -243,7 +247,7 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
|
||||
gui.renderString(gui.Views.Options, actions)
|
||||
var onConfirm func() error
|
||||
if opts.handleConfirmPrompt != nil {
|
||||
onConfirm = gui.wrappedPromptConfirmationFunction(opts.handlersManageFocus, opts.handleConfirmPrompt, func() string { return gui.Views.Confirmation.Buffer() })
|
||||
onConfirm = gui.wrappedPromptConfirmationFunction(opts.handlersManageFocus, opts.handleConfirmPrompt, func() string { return gui.Views.Confirmation.TextArea.GetContent() })
|
||||
} else {
|
||||
onConfirm = gui.wrappedConfirmationFunction(opts.handlersManageFocus, opts.handleConfirm)
|
||||
}
|
||||
@@ -255,7 +259,11 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
|
||||
}
|
||||
|
||||
keybindingConfig := gui.Config.GetUserConfig().Keybinding
|
||||
onSuggestionConfirm := gui.wrappedPromptConfirmationFunction(opts.handlersManageFocus, opts.handleConfirmPrompt, func() string { return gui.getSelectedSuggestionValue() })
|
||||
onSuggestionConfirm := gui.wrappedPromptConfirmationFunction(
|
||||
opts.handlersManageFocus,
|
||||
opts.handleConfirmPrompt,
|
||||
gui.getSelectedSuggestionValue,
|
||||
)
|
||||
|
||||
confirmationKeybindings := []confirmationKeybinding{
|
||||
{
|
||||
@@ -276,7 +284,12 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
|
||||
{
|
||||
viewName: "confirmation",
|
||||
key: gui.getKey(keybindingConfig.Universal.TogglePanel),
|
||||
handler: func() error { return gui.replaceContext(gui.State.Contexts.Suggestions) },
|
||||
handler: func() error {
|
||||
if len(gui.State.Suggestions) > 0 {
|
||||
return gui.replaceContext(gui.State.Contexts.Suggestions)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
viewName: "suggestions",
|
||||
@@ -309,6 +322,16 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) clearConfirmationViewKeyBindings() {
|
||||
keybindingConfig := gui.Config.GetUserConfig().Keybinding
|
||||
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.Confirm), gocui.ModNone)
|
||||
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone)
|
||||
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.Return), gocui.ModNone)
|
||||
_ = gui.g.DeleteKeybinding("suggestions", gui.getKey(keybindingConfig.Universal.Confirm), gocui.ModNone)
|
||||
_ = gui.g.DeleteKeybinding("suggestions", gui.getKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone)
|
||||
_ = gui.g.DeleteKeybinding("suggestions", gui.getKey(keybindingConfig.Universal.Return), gocui.ModNone)
|
||||
}
|
||||
|
||||
func (gui *Gui) wrappedHandler(f func() error) func(g *gocui.Gui, v *gocui.View) error {
|
||||
return func(g *gocui.Gui, v *gocui.View) error {
|
||||
return f()
|
||||
@@ -316,8 +339,7 @@ func (gui *Gui) wrappedHandler(f func() error) func(g *gocui.Gui, v *gocui.View)
|
||||
}
|
||||
|
||||
func (gui *Gui) createErrorPanel(message string) error {
|
||||
colorFunction := color.New(color.FgRed).SprintFunc()
|
||||
coloredMessage := colorFunction(strings.TrimSpace(message))
|
||||
coloredMessage := style.FgRed.Sprint(strings.TrimSpace(message))
|
||||
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+30
-24
@@ -128,33 +128,37 @@ func (gui *Gui) pushContextWithView(viewName string) error {
|
||||
|
||||
func (gui *Gui) returnFromContext() error {
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
gui.State.ContextManager.Lock()
|
||||
|
||||
if len(gui.State.ContextManager.ContextStack) == 1 {
|
||||
// cannot escape from bottommost context
|
||||
gui.State.ContextManager.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
n := len(gui.State.ContextManager.ContextStack) - 1
|
||||
|
||||
currentContext := gui.State.ContextManager.ContextStack[n]
|
||||
newContext := gui.State.ContextManager.ContextStack[n-1]
|
||||
|
||||
gui.State.ContextManager.ContextStack = gui.State.ContextManager.ContextStack[:n]
|
||||
|
||||
gui.State.ContextManager.Unlock()
|
||||
|
||||
if err := gui.deactivateContext(currentContext); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.activateContext(newContext)
|
||||
return gui.returnFromContextSync()
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) returnFromContextSync() error {
|
||||
gui.State.ContextManager.Lock()
|
||||
|
||||
if len(gui.State.ContextManager.ContextStack) == 1 {
|
||||
// cannot escape from bottommost context
|
||||
gui.State.ContextManager.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
n := len(gui.State.ContextManager.ContextStack) - 1
|
||||
|
||||
currentContext := gui.State.ContextManager.ContextStack[n]
|
||||
newContext := gui.State.ContextManager.ContextStack[n-1]
|
||||
|
||||
gui.State.ContextManager.ContextStack = gui.State.ContextManager.ContextStack[:n]
|
||||
|
||||
gui.State.ContextManager.Unlock()
|
||||
|
||||
if err := gui.deactivateContext(currentContext); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.activateContext(newContext)
|
||||
}
|
||||
|
||||
func (gui *Gui) deactivateContext(c Context) error {
|
||||
view, _ := gui.g.View(c.GetViewName())
|
||||
|
||||
@@ -281,9 +285,9 @@ func (gui *Gui) currentContextWithoutLock() Context {
|
||||
|
||||
// the status panel is not yet a list context (and may never be), so this method is not
|
||||
// quite the same as currentSideContext()
|
||||
func (gui *Gui) currentSideListContext() *ListContext {
|
||||
func (gui *Gui) currentSideListContext() IListContext {
|
||||
context := gui.currentSideContext()
|
||||
listContext, ok := context.(*ListContext)
|
||||
listContext, ok := context.(IListContext)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
@@ -395,6 +399,8 @@ func (gui *Gui) onViewFocusLost(oldView *gocui.View, newView *gocui.View) error
|
||||
return nil
|
||||
}
|
||||
|
||||
_ = oldView.SetOriginX(0)
|
||||
|
||||
if oldView == gui.Views.CommitFiles && newView != gui.Views.Main && newView != gui.Views.Secondary && newView != gui.Views.Search {
|
||||
gui.resetWindowForView(gui.Views.CommitFiles)
|
||||
if err := gui.deactivateContext(gui.State.Contexts.CommitFiles); err != nil {
|
||||
|
||||
+13
-13
@@ -56,19 +56,19 @@ var allContextKeys = []ContextKey{
|
||||
|
||||
type ContextTree struct {
|
||||
Status Context
|
||||
Files *ListContext
|
||||
Submodules *ListContext
|
||||
Menu *ListContext
|
||||
Branches *ListContext
|
||||
Remotes *ListContext
|
||||
RemoteBranches *ListContext
|
||||
Tags *ListContext
|
||||
BranchCommits *ListContext
|
||||
CommitFiles *ListContext
|
||||
ReflogCommits *ListContext
|
||||
SubCommits *ListContext
|
||||
Stash *ListContext
|
||||
Suggestions *ListContext
|
||||
Files IListContext
|
||||
Submodules IListContext
|
||||
Menu IListContext
|
||||
Branches IListContext
|
||||
Remotes IListContext
|
||||
RemoteBranches IListContext
|
||||
Tags IListContext
|
||||
BranchCommits IListContext
|
||||
CommitFiles IListContext
|
||||
ReflogCommits IListContext
|
||||
SubCommits IListContext
|
||||
Stash IListContext
|
||||
Suggestions IListContext
|
||||
Normal Context
|
||||
Staging Context
|
||||
PatchBuilding Context
|
||||
|
||||
@@ -41,9 +41,9 @@ func (gui *Gui) promptUserForCredential(passOrUname string) string {
|
||||
|
||||
func (gui *Gui) handleSubmitCredential() error {
|
||||
credentialsView := gui.Views.Credentials
|
||||
message := gui.trimmedContent(credentialsView)
|
||||
message := strings.TrimSpace(credentialsView.TextArea.GetContent())
|
||||
gui.credentials <- message
|
||||
gui.clearEditorView(credentialsView)
|
||||
credentialsView.ClearTextArea()
|
||||
if err := gui.returnFromContext(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+192
-62
@@ -1,13 +1,18 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"log"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -28,6 +33,11 @@ type CustomCommandObjects struct {
|
||||
PromptResponses []string
|
||||
}
|
||||
|
||||
type commandMenuEntry struct {
|
||||
label string
|
||||
value string
|
||||
}
|
||||
|
||||
func (gui *Gui) resolveTemplate(templateStr string, promptResponses []string) (string, error) {
|
||||
objects := CustomCommandObjects{
|
||||
SelectedFile: gui.getSelectedFile(),
|
||||
@@ -49,6 +59,180 @@ func (gui *Gui) resolveTemplate(templateStr string, promptResponses []string) (s
|
||||
return utils.ResolveTemplate(templateStr, objects)
|
||||
}
|
||||
|
||||
func (gui *Gui) inputPrompt(prompt config.CustomCommandPrompt, promptResponses []string, responseIdx int, wrappedF func() error) error {
|
||||
title, err := gui.resolveTemplate(prompt.Title, promptResponses)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
initialValue, err := gui.resolveTemplate(prompt.InitialValue, promptResponses)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.prompt(promptOpts{
|
||||
title: title,
|
||||
initialContent: initialValue,
|
||||
handleConfirm: func(str string) error {
|
||||
promptResponses[responseIdx] = str
|
||||
return wrappedF()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) menuPrompt(prompt config.CustomCommandPrompt, promptResponses []string, responseIdx int, wrappedF func() error) error {
|
||||
// need to make a menu here some how
|
||||
menuItems := make([]*menuItem, len(prompt.Options))
|
||||
for i, option := range prompt.Options {
|
||||
option := option
|
||||
|
||||
nameTemplate := option.Name
|
||||
if nameTemplate == "" {
|
||||
// this allows you to only pass values rather than bother with names/descriptions
|
||||
nameTemplate = option.Value
|
||||
}
|
||||
name, err := gui.resolveTemplate(nameTemplate, promptResponses)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
description, err := gui.resolveTemplate(option.Description, promptResponses)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
value, err := gui.resolveTemplate(option.Value, promptResponses)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
menuItems[i] = &menuItem{
|
||||
displayStrings: []string{name, style.FgYellow.Sprint(description)},
|
||||
onPress: func() error {
|
||||
promptResponses[responseIdx] = value
|
||||
return wrappedF()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
title, err := gui.resolveTemplate(prompt.Title, promptResponses)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.createMenu(title, menuItems, createMenuOptions{showCancel: true})
|
||||
}
|
||||
|
||||
func (gui *Gui) GenerateMenuCandidates(commandOutput, filter, valueFormat, labelFormat string) ([]commandMenuEntry, error) {
|
||||
reg, err := regexp.Compile(filter)
|
||||
if err != nil {
|
||||
return nil, gui.surfaceError(errors.New("unable to parse filter regex, error: " + err.Error()))
|
||||
}
|
||||
|
||||
buff := bytes.NewBuffer(nil)
|
||||
|
||||
valueTemp, err := template.New("format").Parse(valueFormat)
|
||||
if err != nil {
|
||||
return nil, gui.surfaceError(errors.New("unable to parse value format, error: " + err.Error()))
|
||||
}
|
||||
|
||||
colorFuncMap := style.TemplateFuncMapAddColors(template.FuncMap{})
|
||||
|
||||
descTemp, err := template.New("format").Funcs(colorFuncMap).Parse(labelFormat)
|
||||
if err != nil {
|
||||
return nil, gui.surfaceError(errors.New("unable to parse label format, error: " + err.Error()))
|
||||
}
|
||||
|
||||
candidates := []commandMenuEntry{}
|
||||
for _, str := range strings.Split(string(commandOutput), "\n") {
|
||||
if str == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
tmplData := map[string]string{}
|
||||
out := reg.FindAllStringSubmatch(str, -1)
|
||||
if len(out) > 0 {
|
||||
for groupIdx, group := range reg.SubexpNames() {
|
||||
// Record matched group with group ids
|
||||
matchName := "group_" + strconv.Itoa(groupIdx)
|
||||
tmplData[matchName] = out[0][groupIdx]
|
||||
// Record last named group non-empty matches as group matches
|
||||
if group != "" {
|
||||
tmplData[group] = out[0][groupIdx]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = valueTemp.Execute(buff, tmplData)
|
||||
if err != nil {
|
||||
return candidates, gui.surfaceError(err)
|
||||
}
|
||||
entry := commandMenuEntry{
|
||||
value: strings.TrimSpace(buff.String()),
|
||||
}
|
||||
|
||||
if labelFormat != "" {
|
||||
buff.Reset()
|
||||
err = descTemp.Execute(buff, tmplData)
|
||||
if err != nil {
|
||||
return candidates, gui.surfaceError(err)
|
||||
}
|
||||
entry.label = strings.TrimSpace(buff.String())
|
||||
} else {
|
||||
entry.label = entry.value
|
||||
}
|
||||
|
||||
candidates = append(candidates, entry)
|
||||
|
||||
buff.Reset()
|
||||
}
|
||||
return candidates, err
|
||||
}
|
||||
|
||||
func (gui *Gui) menuPromptFromCommand(prompt config.CustomCommandPrompt, promptResponses []string, responseIdx int, wrappedF func() error) error {
|
||||
// Collect cmd to run from config
|
||||
cmdStr, err := gui.resolveTemplate(prompt.Command, promptResponses)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
// Collect Filter regexp
|
||||
filter, err := gui.resolveTemplate(prompt.Filter, promptResponses)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
// Run and save output
|
||||
message, err := gui.GitCommand.RunCommandWithOutput(cmdStr)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
// Need to make a menu out of what the cmd has displayed
|
||||
candidates, err := gui.GenerateMenuCandidates(message, filter, prompt.ValueFormat, prompt.LabelFormat)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
menuItems := make([]*menuItem, len(candidates))
|
||||
for i := range candidates {
|
||||
menuItems[i] = &menuItem{
|
||||
displayStrings: []string{candidates[i].label},
|
||||
onPress: func() error {
|
||||
promptResponses[responseIdx] = candidates[i].value
|
||||
return wrappedF()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
title, err := gui.resolveTemplate(prompt.Title, promptResponses)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.createMenu(title, menuItems, createMenuOptions{showCancel: true})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand) func() error {
|
||||
return func() error {
|
||||
promptResponses := make([]string, len(customCommand.Prompts))
|
||||
@@ -89,72 +273,18 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
|
||||
switch prompt.Type {
|
||||
case "input":
|
||||
f = func() error {
|
||||
title, err := gui.resolveTemplate(prompt.Title, promptResponses)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
initialValue, err := gui.resolveTemplate(prompt.InitialValue, promptResponses)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.prompt(promptOpts{
|
||||
title: title,
|
||||
initialContent: initialValue,
|
||||
handleConfirm: func(str string) error {
|
||||
promptResponses[idx] = str
|
||||
|
||||
return wrappedF()
|
||||
},
|
||||
})
|
||||
return gui.inputPrompt(prompt, promptResponses, idx, wrappedF)
|
||||
}
|
||||
case "menu":
|
||||
f = func() error {
|
||||
// need to make a menu here some how
|
||||
menuItems := make([]*menuItem, len(prompt.Options))
|
||||
for i, option := range prompt.Options {
|
||||
option := option
|
||||
|
||||
nameTemplate := option.Name
|
||||
if nameTemplate == "" {
|
||||
// this allows you to only pass values rather than bother with names/descriptions
|
||||
nameTemplate = option.Value
|
||||
}
|
||||
name, err := gui.resolveTemplate(nameTemplate, promptResponses)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
description, err := gui.resolveTemplate(option.Description, promptResponses)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
value, err := gui.resolveTemplate(option.Value, promptResponses)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
menuItems[i] = &menuItem{
|
||||
displayStrings: []string{name, utils.ColoredString(description, color.FgYellow)},
|
||||
onPress: func() error {
|
||||
promptResponses[idx] = value
|
||||
|
||||
return wrappedF()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
title, err := gui.resolveTemplate(prompt.Title, promptResponses)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.createMenu(title, menuItems, createMenuOptions{showCancel: true})
|
||||
return gui.menuPrompt(prompt, promptResponses, idx, wrappedF)
|
||||
}
|
||||
case "menuFromCommand":
|
||||
f = func() error {
|
||||
return gui.menuPromptFromCommand(prompt, promptResponses, idx, wrappedF)
|
||||
}
|
||||
default:
|
||||
return gui.createErrorPanel("custom command prompt must have a type of 'input' or 'menu'")
|
||||
return gui.createErrorPanel("custom command prompt must have a type of 'input', 'menu' or 'menuFromCommand'")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGuiGenerateMenuCandidates(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
cmdOut string
|
||||
filter string
|
||||
valueFormat string
|
||||
labelFormat string
|
||||
test func([]commandMenuEntry, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Extract remote branch name",
|
||||
"upstream/pr-1",
|
||||
"(?P<remote>[a-z_]+)/(?P<branch>.*)",
|
||||
"{{ .branch }}",
|
||||
"Remote: {{ .remote }}",
|
||||
func(actualEntry []commandMenuEntry, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "pr-1", actualEntry[0].value)
|
||||
assert.EqualValues(t, "Remote: upstream", actualEntry[0].label)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Multiple named groups with empty labelFormat",
|
||||
"upstream/pr-1",
|
||||
"(?P<remote>[a-z]*)/(?P<branch>.*)",
|
||||
"{{ .branch }}|{{ .remote }}",
|
||||
"",
|
||||
func(actualEntry []commandMenuEntry, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "pr-1|upstream", actualEntry[0].value)
|
||||
assert.EqualValues(t, "pr-1|upstream", actualEntry[0].label)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Multiple named groups with group ids",
|
||||
"upstream/pr-1",
|
||||
"(?P<remote>[a-z]*)/(?P<branch>.*)",
|
||||
"{{ .group_2 }}|{{ .group_1 }}",
|
||||
"Remote: {{ .group_1 }}",
|
||||
func(actualEntry []commandMenuEntry, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "pr-1|upstream", actualEntry[0].value)
|
||||
assert.EqualValues(t, "Remote: upstream", actualEntry[0].label)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
s.test(NewDummyGui().GenerateMenuCandidates(s.cmdOut, s.filter, s.valueFormat, s.labelFormat))
|
||||
})
|
||||
}
|
||||
}
|
||||
+6
-3
@@ -3,10 +3,12 @@ package gui
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/modes/diffing"
|
||||
)
|
||||
|
||||
func (gui *Gui) exitDiffMode() error {
|
||||
gui.State.Modes.Diffing = Diffing{}
|
||||
gui.State.Modes.Diffing = diffing.New()
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}
|
||||
|
||||
@@ -123,7 +125,8 @@ func (gui *Gui) handleCreateDiffingMenuPanel() error {
|
||||
displayString: gui.Tr.LcEnterRefToDiff,
|
||||
onPress: func() error {
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.LcEnteRefName,
|
||||
title: gui.Tr.LcEnteRefName,
|
||||
findSuggestionsFunc: gui.getRefsSuggestionsFunc(),
|
||||
handleConfirm: func(response string) error {
|
||||
gui.State.Modes.Diffing.Ref = strings.TrimSpace(response)
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
@@ -145,7 +148,7 @@ func (gui *Gui) handleCreateDiffingMenuPanel() error {
|
||||
{
|
||||
displayString: gui.Tr.LcExitDiffMode,
|
||||
onPress: func() error {
|
||||
gui.State.Modes.Diffing = Diffing{}
|
||||
gui.State.Modes.Diffing = diffing.New()
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/updates"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// NewDummyGui creates a new dummy GUI for testing
|
||||
func NewDummyUpdater() *updates.Updater {
|
||||
newAppConfig := config.NewDummyAppConfig()
|
||||
DummyUpdater, _ := updates.NewUpdater(utils.NewDummyLog(), newAppConfig, oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog(), newAppConfig.GetUserConfig().Gui.Language))
|
||||
return DummyUpdater
|
||||
}
|
||||
|
||||
func NewDummyGui() *Gui {
|
||||
newAppConfig := config.NewDummyAppConfig()
|
||||
DummyGui, _ := NewGui(utils.NewDummyLog(), commands.NewDummyGitCommand(), oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog(), newAppConfig.GetUserConfig().Gui.Language), newAppConfig, NewDummyUpdater(), "", false)
|
||||
return DummyGui
|
||||
}
|
||||
+44
-53
@@ -6,90 +6,81 @@ import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
// we've just copy+pasted the editor from gocui to here so that we can also re-
|
||||
// render the commit message length on each keypress
|
||||
func (gui *Gui) commitMessageEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
|
||||
func (gui *Gui) handleEditorKeypress(textArea *gocui.TextArea, key gocui.Key, ch rune, mod gocui.Modifier, allowMultiline bool) bool {
|
||||
newlineKey, ok := gui.getKey(gui.Config.GetUserConfig().Keybinding.Universal.AppendNewline).(gocui.Key)
|
||||
if !ok {
|
||||
newlineKey = gocui.KeyAltEnter
|
||||
}
|
||||
|
||||
matched := true
|
||||
switch {
|
||||
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
|
||||
v.EditDelete(true)
|
||||
textArea.BackSpaceChar()
|
||||
case key == gocui.KeyCtrlD || key == gocui.KeyDelete:
|
||||
v.EditDelete(false)
|
||||
textArea.DeleteChar()
|
||||
case key == gocui.KeyArrowDown:
|
||||
v.MoveCursor(0, 1, false)
|
||||
textArea.MoveCursorDown()
|
||||
case key == gocui.KeyArrowUp:
|
||||
v.MoveCursor(0, -1, false)
|
||||
textArea.MoveCursorUp()
|
||||
case key == gocui.KeyArrowLeft:
|
||||
v.MoveCursor(-1, 0, false)
|
||||
textArea.MoveCursorLeft()
|
||||
case key == gocui.KeyArrowRight:
|
||||
v.MoveCursor(1, 0, false)
|
||||
textArea.MoveCursorRight()
|
||||
case key == newlineKey:
|
||||
v.EditNewLine()
|
||||
if allowMultiline {
|
||||
textArea.TypeRune('\n')
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case key == gocui.KeySpace:
|
||||
v.EditWrite(' ')
|
||||
textArea.TypeRune(' ')
|
||||
case key == gocui.KeyInsert:
|
||||
v.Overwrite = !v.Overwrite
|
||||
textArea.ToggleOverwrite()
|
||||
case key == gocui.KeyCtrlU:
|
||||
v.EditDeleteToStartOfLine()
|
||||
case key == gocui.KeyCtrlA:
|
||||
v.EditGotoToStartOfLine()
|
||||
case key == gocui.KeyCtrlE:
|
||||
v.EditGotoToEndOfLine()
|
||||
textArea.DeleteToStartOfLine()
|
||||
case key == gocui.KeyCtrlA || key == gocui.KeyHome:
|
||||
textArea.GoToStartOfLine()
|
||||
case key == gocui.KeyCtrlE || key == gocui.KeyEnd:
|
||||
textArea.GoToEndOfLine()
|
||||
|
||||
// TODO: see if we need all three of these conditions: maybe the final one is sufficient
|
||||
case ch != 0 && mod == 0 && unicode.IsPrint(ch):
|
||||
v.EditWrite(ch)
|
||||
textArea.TypeRune(ch)
|
||||
default:
|
||||
matched = false
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// we've just copy+pasted the editor from gocui to here so that we can also re-
|
||||
// render the commit message length on each keypress
|
||||
func (gui *Gui) commitMessageEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
|
||||
matched := gui.handleEditorKeypress(v.TextArea, key, ch, mod, true)
|
||||
|
||||
// This function is called again on refresh as part of the general resize popup call,
|
||||
// but we need to call it here so that when we go to render the text area it's not
|
||||
// considered out of bounds to add a newline, meaning we can avoid unnecessary scrolling.
|
||||
err := gui.resizePopupPanel(v, v.TextArea.GetContent())
|
||||
if err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
v.RenderTextArea()
|
||||
gui.RenderCommitLength()
|
||||
|
||||
return matched
|
||||
}
|
||||
|
||||
func (gui *Gui) defaultEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
|
||||
matched := true
|
||||
switch {
|
||||
case key == gocui.KeyBackspace || key == gocui.KeyBackspace2:
|
||||
v.EditDelete(true)
|
||||
case key == gocui.KeyCtrlD || key == gocui.KeyDelete:
|
||||
v.EditDelete(false)
|
||||
case key == gocui.KeyArrowDown:
|
||||
v.MoveCursor(0, 1, false)
|
||||
case key == gocui.KeyArrowUp:
|
||||
v.MoveCursor(0, -1, false)
|
||||
case key == gocui.KeyArrowLeft:
|
||||
v.MoveCursor(-1, 0, false)
|
||||
case key == gocui.KeyArrowRight:
|
||||
v.MoveCursor(1, 0, false)
|
||||
case key == gocui.KeySpace:
|
||||
v.EditWrite(' ')
|
||||
case key == gocui.KeyInsert:
|
||||
v.Overwrite = !v.Overwrite
|
||||
case key == gocui.KeyCtrlU:
|
||||
v.EditDeleteToStartOfLine()
|
||||
case key == gocui.KeyCtrlA:
|
||||
v.EditGotoToStartOfLine()
|
||||
case key == gocui.KeyCtrlE:
|
||||
v.EditGotoToEndOfLine()
|
||||
matched := gui.handleEditorKeypress(v.TextArea, key, ch, mod, false)
|
||||
|
||||
// TODO: see if we need all three of these conditions: maybe the final one is sufficient
|
||||
case ch != 0 && mod == 0 && unicode.IsPrint(ch):
|
||||
v.EditWrite(ch)
|
||||
default:
|
||||
matched = false
|
||||
}
|
||||
v.RenderTextArea()
|
||||
|
||||
if gui.findSuggestions != nil {
|
||||
input := v.Buffer()
|
||||
suggestions := gui.findSuggestions(input)
|
||||
gui.setSuggestions(suggestions)
|
||||
input := v.TextArea.GetContent()
|
||||
gui.suggestionsAsyncHandler.Do(func() func() {
|
||||
suggestions := gui.findSuggestions(input)
|
||||
return func() { gui.setSuggestions(suggestions) }
|
||||
})
|
||||
}
|
||||
|
||||
return matched
|
||||
|
||||
+36
-4
@@ -1,5 +1,11 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
)
|
||||
|
||||
func (gui *Gui) handleCreateExtrasMenuPanel() error {
|
||||
menuItems := []*menuItem{
|
||||
{
|
||||
@@ -11,15 +17,16 @@ func (gui *Gui) handleCreateExtrasMenuPanel() error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
gui.ShowExtrasWindow = !gui.ShowExtrasWindow
|
||||
show := !gui.ShowExtrasWindow
|
||||
gui.ShowExtrasWindow = show
|
||||
gui.Config.GetAppState().HideCommandLog = !show
|
||||
_ = gui.Config.SaveAppState()
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: gui.Tr.FocusCommandLog,
|
||||
onPress: func() error {
|
||||
return gui.handleFocusCommandLog()
|
||||
},
|
||||
onPress: gui.handleFocusCommandLog,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -47,3 +54,28 @@ func (gui *Gui) scrollDownExtra() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) getCmdWriter() io.Writer {
|
||||
return &prefixWriter{writer: gui.Views.Extras, prefix: style.FgMagenta.Sprintf("\n\n%s\n", gui.Tr.GitOutput)}
|
||||
}
|
||||
|
||||
// Ensures that the first write is preceded by writing a prefix.
|
||||
// This allows us to say 'Git output:' before writing the actual git output.
|
||||
// We could just write directly to the view in this package before running the command but we already have code in the commands package that writes to the same view beforehand (with the command it's about to run) so things would be out of order.
|
||||
type prefixWriter struct {
|
||||
prefix string
|
||||
prefixWritten bool
|
||||
writer io.Writer
|
||||
}
|
||||
|
||||
func (self *prefixWriter) Write(p []byte) (n int, err error) {
|
||||
if !self.prefixWritten {
|
||||
self.prefixWritten = true
|
||||
// assuming we can write this prefix in one go
|
||||
_, err = self.writer.Write([]byte(self.prefix))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return self.writer.Write(p)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
// macs for some bizarre reason cap the number of watchable files to 256.
|
||||
// there's no obvious platform agonstic way to check the situation of the user's
|
||||
// there's no obvious platform agnostic way to check the situation of the user's
|
||||
// computer so we're just arbitrarily capping at 200. This isn't so bad because
|
||||
// file watching is only really an added bonus for faster refreshing.
|
||||
const MAX_WATCHED_FILES = 50
|
||||
|
||||
+154
-68
@@ -57,13 +57,6 @@ func (gui *Gui) selectFile(alreadySelected bool) error {
|
||||
}
|
||||
|
||||
if !alreadySelected {
|
||||
// TODO: pull into update task interface
|
||||
if err := gui.resetOrigin(gui.Views.Main); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.resetOrigin(gui.Views.Secondary); err != nil {
|
||||
return err
|
||||
}
|
||||
gui.takeOverMergeConflictScrolling()
|
||||
}
|
||||
|
||||
@@ -71,7 +64,7 @@ func (gui *Gui) selectFile(alreadySelected bool) error {
|
||||
return gui.refreshMergePanelWithLock()
|
||||
}
|
||||
|
||||
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(node, false, !node.GetHasUnstagedChanges() && node.GetHasStagedChanges())
|
||||
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(node, false, !node.GetHasUnstagedChanges() && node.GetHasStagedChanges(), gui.State.IgnoreWhitespaceInDiffView)
|
||||
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
|
||||
|
||||
refreshOpts := refreshMainOpts{main: &viewUpdateOpts{
|
||||
@@ -81,7 +74,7 @@ func (gui *Gui) selectFile(alreadySelected bool) error {
|
||||
|
||||
if node.GetHasUnstagedChanges() {
|
||||
if node.GetHasStagedChanges() {
|
||||
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(node, false, true)
|
||||
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(node, false, true, gui.State.IgnoreWhitespaceInDiffView)
|
||||
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
|
||||
|
||||
refreshOpts.secondary = &viewUpdateOpts{
|
||||
@@ -347,9 +340,10 @@ func (gui *Gui) handleWIPCommitPress() error {
|
||||
return gui.createErrorPanel(gui.Tr.SkipHookPrefixNotConfigured)
|
||||
}
|
||||
|
||||
if err := gui.Views.CommitMessage.SetEditorContent(skipHookPrefix); err != nil {
|
||||
return err
|
||||
}
|
||||
textArea := gui.Views.CommitMessage.TextArea
|
||||
textArea.Clear()
|
||||
textArea.TypeString(skipHookPrefix)
|
||||
gui.Views.CommitMessage.RenderTextArea()
|
||||
|
||||
return gui.handleCommitPress()
|
||||
}
|
||||
@@ -399,10 +393,9 @@ func (gui *Gui) handleCommitPress() error {
|
||||
return gui.createErrorPanel(fmt.Sprintf("%s: %s", gui.Tr.LcCommitPrefixPatternError, err.Error()))
|
||||
}
|
||||
prefix := rgx.ReplaceAllString(gui.getCheckedOutBranch().Name, prefixReplace)
|
||||
gui.renderString(gui.Views.CommitMessage, prefix)
|
||||
if err := gui.Views.CommitMessage.SetCursor(len(prefix), 0); err != nil {
|
||||
return err
|
||||
}
|
||||
gui.Views.CommitMessage.ClearTextArea()
|
||||
gui.Views.CommitMessage.TextArea.TypeString(prefix)
|
||||
gui.render()
|
||||
}
|
||||
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
@@ -473,14 +466,49 @@ func (gui *Gui) handleCommitEditorPress() error {
|
||||
)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStatusFilterPressed() error {
|
||||
menuItems := []*menuItem{
|
||||
{
|
||||
displayString: gui.Tr.FilterStagedFiles,
|
||||
onPress: func() error {
|
||||
return gui.setStatusFiltering(filetree.DisplayStaged)
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: gui.Tr.FilterUnstagedFiles,
|
||||
onPress: func() error {
|
||||
return gui.setStatusFiltering(filetree.DisplayUnstaged)
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: gui.Tr.ResetCommitFilterState,
|
||||
onPress: func() error {
|
||||
return gui.setStatusFiltering(filetree.DisplayAll)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return gui.createMenu(gui.Tr.FilteringMenuTitle, menuItems, createMenuOptions{showCancel: true})
|
||||
}
|
||||
|
||||
func (gui *Gui) setStatusFiltering(filter filetree.FileManagerDisplayFilter) error {
|
||||
state := gui.State
|
||||
state.FileManager.SetDisplayFilter(filter)
|
||||
return gui.handleRefreshFiles()
|
||||
}
|
||||
|
||||
func (gui *Gui) editFile(filename string) error {
|
||||
cmdStr, err := gui.GitCommand.EditFileCmdStr(filename)
|
||||
return gui.editFileAtLine(filename, 1)
|
||||
}
|
||||
|
||||
func (gui *Gui) editFileAtLine(filename string, lineNumber int) error {
|
||||
cmdStr, err := gui.GitCommand.EditFileCmdStr(filename, lineNumber)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(
|
||||
gui.OSCommand.WithSpan(gui.Tr.Spans.EditFile).PrepareShellSubProcess(cmdStr),
|
||||
gui.OSCommand.WithSpan(gui.Tr.Spans.EditFile).ShellCommandFromString(cmdStr),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -617,7 +645,7 @@ func (gui *Gui) handlePullFiles() error {
|
||||
}
|
||||
|
||||
// if we have no upstream branch we need to set that first
|
||||
if currentBranch.Pullables == "?" {
|
||||
if !currentBranch.IsTrackingRemote() {
|
||||
// see if we have this branch in our config with an upstream
|
||||
conf, err := gui.GitCommand.Repo.Config()
|
||||
if err != nil {
|
||||
@@ -630,8 +658,9 @@ func (gui *Gui) handlePullFiles() error {
|
||||
}
|
||||
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.EnterUpstream,
|
||||
initialContent: "origin/" + currentBranch.Name,
|
||||
title: gui.Tr.EnterUpstream,
|
||||
initialContent: "origin/" + currentBranch.Name,
|
||||
findSuggestionsFunc: gui.getRemoteBranchesSuggestionsFunc("/"),
|
||||
handleConfirm: func(upstream string) error {
|
||||
if err := gui.GitCommand.SetUpstreamBranch(upstream); err != nil {
|
||||
errorMessage := err.Error()
|
||||
@@ -649,9 +678,10 @@ func (gui *Gui) handlePullFiles() error {
|
||||
}
|
||||
|
||||
type PullFilesOptions struct {
|
||||
RemoteName string
|
||||
BranchName string
|
||||
span string
|
||||
RemoteName string
|
||||
BranchName string
|
||||
FastForwardOnly bool
|
||||
span string
|
||||
}
|
||||
|
||||
func (gui *Gui) pullFiles(opts PullFilesOptions) error {
|
||||
@@ -659,55 +689,53 @@ func (gui *Gui) pullFiles(opts PullFilesOptions) error {
|
||||
return err
|
||||
}
|
||||
|
||||
mode := gui.Config.GetUserConfig().Git.Pull.Mode
|
||||
|
||||
// TODO: this doesn't look like a good idea. Why the goroutine?
|
||||
go utils.Safe(func() { _ = gui.pullWithMode(mode, opts) })
|
||||
go utils.Safe(func() { _ = gui.pullWithLock(opts) })
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) pullWithMode(mode string, opts PullFilesOptions) error {
|
||||
func (gui *Gui) pullWithLock(opts PullFilesOptions) error {
|
||||
gui.Mutexes.FetchMutex.Lock()
|
||||
defer gui.Mutexes.FetchMutex.Unlock()
|
||||
|
||||
gitCommand := gui.GitCommand.WithSpan(opts.span)
|
||||
|
||||
err := gitCommand.Fetch(
|
||||
commands.FetchOptions{
|
||||
err := gitCommand.Pull(
|
||||
commands.PullOptions{
|
||||
PromptUserForCredential: gui.promptUserForCredential,
|
||||
RemoteName: opts.RemoteName,
|
||||
BranchName: opts.BranchName,
|
||||
FastForwardOnly: opts.FastForwardOnly,
|
||||
},
|
||||
)
|
||||
gui.handleCredentialsPopup(err)
|
||||
if err != nil {
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case "rebase":
|
||||
err := gitCommand.RebaseBranch("FETCH_HEAD")
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
case "merge":
|
||||
err := gitCommand.Merge("FETCH_HEAD", commands.MergeOpts{})
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
case "ff-only":
|
||||
err := gitCommand.Merge("FETCH_HEAD", commands.MergeOpts{FastForwardOnly: true})
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
default:
|
||||
return gui.createErrorPanel(fmt.Sprintf("git pull mode '%s' unrecognised", mode))
|
||||
if err == nil {
|
||||
_ = gui.closeConfirmationPrompt(false)
|
||||
}
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
}
|
||||
|
||||
func (gui *Gui) pushWithForceFlag(force bool, upstream string, args string) error {
|
||||
type pushOpts struct {
|
||||
force bool
|
||||
upstreamRemote string
|
||||
upstreamBranch string
|
||||
setUpstream bool
|
||||
}
|
||||
|
||||
func (gui *Gui) push(opts pushOpts) error {
|
||||
if err := gui.createLoaderPanel(gui.Tr.PushWait); err != nil {
|
||||
return err
|
||||
}
|
||||
go utils.Safe(func() {
|
||||
branchName := gui.getCheckedOutBranch().Name
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.Push).Push(branchName, force, upstream, args, gui.promptUserForCredential)
|
||||
if err != nil && !force && strings.Contains(err.Error(), "Updates were rejected") {
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.Push).Push(commands.PushOpts{
|
||||
Force: opts.force,
|
||||
UpstreamRemote: opts.upstreamRemote,
|
||||
UpstreamBranch: opts.upstreamBranch,
|
||||
SetUpstream: opts.setUpstream,
|
||||
PromptUserForCredential: gui.promptUserForCredential,
|
||||
})
|
||||
|
||||
if err != nil && !opts.force && strings.Contains(err.Error(), "Updates were rejected") {
|
||||
forcePushDisabled := gui.Config.GetUserConfig().Git.DisableForcePushing
|
||||
if forcePushDisabled {
|
||||
_ = gui.createErrorPanel(gui.Tr.UpdatesRejectedAndForcePushDisabled)
|
||||
@@ -717,7 +745,10 @@ func (gui *Gui) pushWithForceFlag(force bool, upstream string, args string) erro
|
||||
title: gui.Tr.ForcePush,
|
||||
prompt: gui.Tr.ForcePushPrompt,
|
||||
handleConfirm: func() error {
|
||||
return gui.pushWithForceFlag(true, upstream, args)
|
||||
newOpts := opts
|
||||
newOpts.force = true
|
||||
|
||||
return gui.push(newOpts)
|
||||
},
|
||||
})
|
||||
return
|
||||
@@ -740,33 +771,60 @@ func (gui *Gui) pushFiles() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if currentBranch.Pullables == "?" {
|
||||
// see if we have this branch in our config with an upstream
|
||||
conf, err := gui.GitCommand.Repo.Config()
|
||||
if currentBranch.IsTrackingRemote() {
|
||||
if currentBranch.HasCommitsToPull() {
|
||||
return gui.requestToForcePush()
|
||||
} else {
|
||||
return gui.push(pushOpts{})
|
||||
}
|
||||
} else {
|
||||
// see if we have an upstream for this branch in our config
|
||||
upstreamRemote, upstreamBranch, err := gui.upstreamForBranchInConfig(currentBranch.Name)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
for branchName, branch := range conf.Branches {
|
||||
if branchName == currentBranch.Name {
|
||||
return gui.pushWithForceFlag(false, "", fmt.Sprintf("%s %s", branch.Remote, branchName))
|
||||
}
|
||||
|
||||
if upstreamBranch != "" {
|
||||
return gui.push(
|
||||
pushOpts{
|
||||
force: false,
|
||||
upstreamRemote: upstreamRemote,
|
||||
upstreamBranch: upstreamBranch,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if gui.GitCommand.PushToCurrent {
|
||||
return gui.pushWithForceFlag(false, "", "--set-upstream")
|
||||
return gui.push(pushOpts{setUpstream: true})
|
||||
} else {
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.EnterUpstream,
|
||||
initialContent: "origin " + currentBranch.Name,
|
||||
handleConfirm: func(response string) error {
|
||||
return gui.pushWithForceFlag(false, response, "")
|
||||
title: gui.Tr.EnterUpstream,
|
||||
initialContent: "origin " + currentBranch.Name,
|
||||
findSuggestionsFunc: gui.getRemoteBranchesSuggestionsFunc(" "),
|
||||
handleConfirm: func(upstream string) error {
|
||||
var upstreamBranch, upstreamRemote string
|
||||
split := strings.Split(upstream, " ")
|
||||
if len(split) == 2 {
|
||||
upstreamRemote = split[0]
|
||||
upstreamBranch = split[1]
|
||||
} else {
|
||||
upstreamRemote = upstream
|
||||
upstreamBranch = ""
|
||||
}
|
||||
|
||||
return gui.push(pushOpts{
|
||||
force: false,
|
||||
upstreamRemote: upstreamRemote,
|
||||
upstreamBranch: upstreamBranch,
|
||||
setUpstream: true,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
} else if currentBranch.Pullables == "0" {
|
||||
return gui.pushWithForceFlag(false, "", "")
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) requestToForcePush() error {
|
||||
forcePushDisabled := gui.Config.GetUserConfig().Git.DisableForcePushing
|
||||
if forcePushDisabled {
|
||||
return gui.createErrorPanel(gui.Tr.ForcePushDisabled)
|
||||
@@ -776,11 +834,26 @@ func (gui *Gui) pushFiles() error {
|
||||
title: gui.Tr.ForcePush,
|
||||
prompt: gui.Tr.ForcePushPrompt,
|
||||
handleConfirm: func() error {
|
||||
return gui.pushWithForceFlag(true, "", "")
|
||||
return gui.push(pushOpts{force: true})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) upstreamForBranchInConfig(branchName string) (string, string, error) {
|
||||
conf, err := gui.GitCommand.Repo.Config()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
for configBranchName, configBranch := range conf.Branches {
|
||||
if configBranchName == branchName {
|
||||
return configBranch.Remote, configBranchName, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSwitchToMerge() error {
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
@@ -812,8 +885,21 @@ func (gui *Gui) anyFilesWithMergeConflicts() bool {
|
||||
|
||||
func (gui *Gui) handleCustomCommand() error {
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.CustomCommand,
|
||||
title: gui.Tr.CustomCommand,
|
||||
findSuggestionsFunc: gui.getCustomCommandsHistorySuggestionsFunc(),
|
||||
handleConfirm: func(command string) error {
|
||||
gui.Config.GetAppState().CustomCommandsHistory = utils.Limit(
|
||||
utils.Uniq(
|
||||
append(gui.Config.GetAppState().CustomCommandsHistory, command),
|
||||
),
|
||||
1000,
|
||||
)
|
||||
|
||||
err := gui.Config.SaveAppState()
|
||||
if err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(command, gui.Tr.Spans.CustomCommand, true))
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(
|
||||
gui.OSCommand.PrepareShellSubProcess(command),
|
||||
|
||||
@@ -54,23 +54,33 @@ func TestBuildTreeFromFiles(t *testing.T) {
|
||||
name: "paths that can be compressed",
|
||||
files: []*models.File{
|
||||
{
|
||||
Name: "dir1/a",
|
||||
Name: "dir1/dir3/a",
|
||||
},
|
||||
{
|
||||
Name: "dir2/b",
|
||||
Name: "dir2/dir4/b",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
Path: "dir1/dir3",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "dir1/dir3/a"},
|
||||
Path: "dir1/dir3/a",
|
||||
},
|
||||
},
|
||||
CompressionLevel: 1,
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "dir2/b"},
|
||||
Path: "dir2/b",
|
||||
Path: "dir2/dir4",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "dir2/dir4/b"},
|
||||
Path: "dir2/dir4/b",
|
||||
},
|
||||
},
|
||||
CompressionLevel: 1,
|
||||
},
|
||||
},
|
||||
@@ -201,12 +211,12 @@ func TestBuildFlatTreeFromFiles(t *testing.T) {
|
||||
{
|
||||
File: &models.File{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
CompressionLevel: 1,
|
||||
CompressionLevel: 0,
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "dir2/b"},
|
||||
Path: "dir2/b",
|
||||
CompressionLevel: 1,
|
||||
CompressionLevel: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -351,23 +361,33 @@ func TestBuildTreeFromCommitFiles(t *testing.T) {
|
||||
name: "paths that can be compressed",
|
||||
files: []*models.CommitFile{
|
||||
{
|
||||
Name: "dir1/a",
|
||||
Name: "dir1/dir3/a",
|
||||
},
|
||||
{
|
||||
Name: "dir2/b",
|
||||
Name: "dir2/dir4/b",
|
||||
},
|
||||
},
|
||||
expected: &CommitFileNode{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
Path: "dir1/dir3",
|
||||
Children: []*CommitFileNode{
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir1/dir3/a"},
|
||||
Path: "dir1/dir3/a",
|
||||
},
|
||||
},
|
||||
CompressionLevel: 1,
|
||||
},
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir2/b"},
|
||||
Path: "dir2/b",
|
||||
Path: "dir2/dir4",
|
||||
Children: []*CommitFileNode{
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir2/dir4/b"},
|
||||
Path: "dir2/dir4/b",
|
||||
},
|
||||
},
|
||||
CompressionLevel: 1,
|
||||
},
|
||||
},
|
||||
@@ -464,12 +484,12 @@ func TestBuildFlatTreeFromCommitFiles(t *testing.T) {
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
CompressionLevel: 1,
|
||||
CompressionLevel: 0,
|
||||
},
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir2/b"},
|
||||
Path: "dir2/b",
|
||||
CompressionLevel: 1,
|
||||
CompressionLevel: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -8,11 +8,20 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type FileManagerDisplayFilter int
|
||||
|
||||
const (
|
||||
DisplayAll FileManagerDisplayFilter = iota
|
||||
DisplayStaged
|
||||
DisplayUnstaged
|
||||
)
|
||||
|
||||
type FileManager struct {
|
||||
files []*models.File
|
||||
tree *FileNode
|
||||
showTree bool
|
||||
log *logrus.Entry
|
||||
filter FileManagerDisplayFilter
|
||||
collapsedPaths CollapsedPaths
|
||||
sync.RWMutex
|
||||
}
|
||||
@@ -22,6 +31,7 @@ func NewFileManager(files []*models.File, log *logrus.Entry, showTree bool) *Fil
|
||||
files: files,
|
||||
log: log,
|
||||
showTree: showTree,
|
||||
filter: DisplayAll,
|
||||
collapsedPaths: CollapsedPaths{},
|
||||
RWMutex: sync.RWMutex{},
|
||||
}
|
||||
@@ -35,6 +45,35 @@ func (m *FileManager) ExpandToPath(path string) {
|
||||
m.collapsedPaths.ExpandToPath(path)
|
||||
}
|
||||
|
||||
func (m *FileManager) GetFilesForDisplay() []*models.File {
|
||||
files := m.files
|
||||
if m.filter == DisplayAll {
|
||||
return files
|
||||
}
|
||||
|
||||
result := make([]*models.File, 0)
|
||||
if m.filter == DisplayStaged {
|
||||
for _, file := range files {
|
||||
if file.HasStagedChanges {
|
||||
result = append(result, file)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, file := range files {
|
||||
if !file.HasStagedChanges {
|
||||
result = append(result, file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (m *FileManager) SetDisplayFilter(filter FileManagerDisplayFilter) {
|
||||
m.filter = filter
|
||||
m.SetTree()
|
||||
}
|
||||
|
||||
func (m *FileManager) ToggleShowTree() {
|
||||
m.showTree = !m.showTree
|
||||
m.SetTree()
|
||||
@@ -73,10 +112,11 @@ func (m *FileManager) SetFiles(files []*models.File) {
|
||||
}
|
||||
|
||||
func (m *FileManager) SetTree() {
|
||||
filesForDisplay := m.GetFilesForDisplay()
|
||||
if m.showTree {
|
||||
m.tree = BuildTreeFromFiles(m.files)
|
||||
m.tree = BuildTreeFromFiles(filesForDisplay)
|
||||
} else {
|
||||
m.tree = BuildFlatTreeFromFiles(m.files)
|
||||
m.tree = BuildFlatTreeFromFiles(filesForDisplay)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,16 @@ package filetree
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gookit/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/xo/terminfo"
|
||||
)
|
||||
|
||||
func init() {
|
||||
color.ForceSetColorLevel(terminfo.ColorLevelNone)
|
||||
}
|
||||
|
||||
func TestRender(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
@@ -89,3 +95,62 @@ func TestRender(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterAction(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
filter FileManagerDisplayFilter
|
||||
files []*models.File
|
||||
expected []*models.File
|
||||
}{
|
||||
{
|
||||
name: "filter files with unstaged changes",
|
||||
filter: DisplayUnstaged,
|
||||
files: []*models.File{
|
||||
{Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Name: "dir2/file5", ShortStatus: "M ", HasStagedChanges: true},
|
||||
{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
},
|
||||
expected: []*models.File{
|
||||
{Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "filter files with staged changes",
|
||||
filter: DisplayStaged,
|
||||
files: []*models.File{
|
||||
{Name: "dir2/dir2/file4", ShortStatus: "M ", HasStagedChanges: true},
|
||||
{Name: "dir2/file5", ShortStatus: "M ", HasStagedChanges: false},
|
||||
{Name: "file1", ShortStatus: "M ", HasStagedChanges: true},
|
||||
},
|
||||
expected: []*models.File{
|
||||
{Name: "dir2/dir2/file4", ShortStatus: "M ", HasStagedChanges: true},
|
||||
{Name: "file1", ShortStatus: "M ", HasStagedChanges: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "filter all files",
|
||||
filter: DisplayAll,
|
||||
files: []*models.File{
|
||||
{Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Name: "dir2/file5", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
},
|
||||
expected: []*models.File{
|
||||
{Name: "dir2/dir2/file4", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Name: "dir2/file5", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
mngr := &FileManager{files: s.files, filter: s.filter}
|
||||
result := mngr.GetFilesForDisplay()
|
||||
assert.EqualValues(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
)
|
||||
|
||||
@@ -182,7 +180,7 @@ func (s *FileNode) NameAtDepth(depth int) string {
|
||||
prevName = join(splitPrevName[depth:])
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s%s%s", prevName, " → ", name)
|
||||
return prevName + " → " + name
|
||||
}
|
||||
|
||||
return name
|
||||
|
||||
@@ -84,9 +84,13 @@ func TestCompress(t *testing.T) {
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
Path: "dir1/file2",
|
||||
File: &models.File{Name: "file2", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
CompressionLevel: 1,
|
||||
Path: "dir1",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "file2", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "dir1/file2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "dir2",
|
||||
@@ -102,9 +106,14 @@ func TestCompress(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "dir3/dir3-1/file5",
|
||||
File: &models.File{Name: "file5", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
CompressionLevel: 2,
|
||||
Path: "dir3/dir3-1",
|
||||
CompressionLevel: 1,
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "file5", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
Path: "dir3/dir3-1/file5",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "file1", ShortStatus: "M ", HasUnstagedChanges: true},
|
||||
|
||||
@@ -170,7 +170,7 @@ func compressAux(node INode) INode {
|
||||
children := node.GetChildren()
|
||||
for i := range children {
|
||||
grandchildren := children[i].GetChildren()
|
||||
for len(grandchildren) == 1 {
|
||||
for len(grandchildren) == 1 && !grandchildren[0].IsLeaf() {
|
||||
grandchildren[0].SetCompressionLevel(children[i].GetCompressionLevel() + 1)
|
||||
children[i] = grandchildren[0]
|
||||
grandchildren = children[i].GetChildren()
|
||||
|
||||
@@ -35,7 +35,8 @@ func (gui *Gui) handleCreateFilteringMenuPanel() error {
|
||||
displayString: gui.Tr.LcFilterPathOption,
|
||||
onPress: func() error {
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.LcEnterFileName,
|
||||
findSuggestionsFunc: gui.getFilePathSuggestionsFunc(),
|
||||
title: gui.Tr.EnterFileName,
|
||||
handleConfirm: func(response string) error {
|
||||
return gui.setFiltering(strings.TrimSpace(response))
|
||||
},
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/jesseduffield/minimal/gitignore"
|
||||
"gopkg.in/ozeidan/fuzzy-patricia.v3/patricia"
|
||||
)
|
||||
|
||||
// Thinking out loud: I'm typically a staunch advocate of organising code by feature rather than type,
|
||||
// because colocating code that relates to the same feature means far less effort
|
||||
// to get all the context you need to work on any particular feature. But the one
|
||||
// major benefit of grouping by type is that it makes it makes it less likely that
|
||||
// somebody will re-implement the same logic twice, because they can quickly see
|
||||
// if a certain method has been used for some use case, given that as a starting point
|
||||
// they know about the type. In that vein, I'm including all our functions for
|
||||
// finding suggestions in this file, so that it's easy to see if a function already
|
||||
// exists for fetching a particular model.
|
||||
|
||||
func (gui *Gui) getRemoteNames() []string {
|
||||
result := make([]string, len(gui.State.Remotes))
|
||||
for i, remote := range gui.State.Remotes {
|
||||
result[i] = remote.Name
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func matchesToSuggestions(matches []string) []*types.Suggestion {
|
||||
suggestions := make([]*types.Suggestion, len(matches))
|
||||
for i, match := range matches {
|
||||
suggestions[i] = &types.Suggestion{
|
||||
Value: match,
|
||||
Label: match,
|
||||
}
|
||||
}
|
||||
return suggestions
|
||||
}
|
||||
|
||||
func (gui *Gui) getRemoteSuggestionsFunc() func(string) []*types.Suggestion {
|
||||
remoteNames := gui.getRemoteNames()
|
||||
|
||||
return fuzzySearchFunc(remoteNames)
|
||||
}
|
||||
|
||||
func (gui *Gui) getBranchNames() []string {
|
||||
result := make([]string, len(gui.State.Branches))
|
||||
for i, branch := range gui.State.Branches {
|
||||
result[i] = branch.Name
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (gui *Gui) getBranchNameSuggestionsFunc() func(string) []*types.Suggestion {
|
||||
branchNames := gui.getBranchNames()
|
||||
|
||||
return func(input string) []*types.Suggestion {
|
||||
var matchingBranchNames []string
|
||||
if input == "" {
|
||||
matchingBranchNames = branchNames
|
||||
} else {
|
||||
matchingBranchNames = utils.FuzzySearch(input, branchNames)
|
||||
}
|
||||
|
||||
suggestions := make([]*types.Suggestion, len(matchingBranchNames))
|
||||
for i, branchName := range matchingBranchNames {
|
||||
suggestions[i] = &types.Suggestion{
|
||||
Value: branchName,
|
||||
Label: presentation.GetBranchTextStyle(branchName).Sprint(branchName),
|
||||
}
|
||||
}
|
||||
|
||||
return suggestions
|
||||
}
|
||||
}
|
||||
|
||||
// here we asynchronously fetch the latest set of paths in the repo and store in
|
||||
// gui.State.FilesTrie. On the main thread we'll be doing a fuzzy search via
|
||||
// gui.State.FilesTrie. So if we've looked for a file previously, we'll start with
|
||||
// the old trie and eventually it'll be swapped out for the new one.
|
||||
// Notably, unlike other suggestion functions we're not showing all the options
|
||||
// if nothing has been typed because there'll be too much to display efficiently
|
||||
func (gui *Gui) getFilePathSuggestionsFunc() func(string) []*types.Suggestion {
|
||||
_ = gui.WithWaitingStatus(gui.Tr.LcLoadingFileSuggestions, func() error {
|
||||
trie := patricia.NewTrie()
|
||||
// load every non-gitignored file in the repo
|
||||
ignore, err := gitignore.FromGit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ignore.Walk(".",
|
||||
func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
trie.Insert(patricia.Prefix(path), path)
|
||||
return nil
|
||||
})
|
||||
// cache the trie for future use
|
||||
gui.State.FilesTrie = trie
|
||||
|
||||
// refresh the selections view
|
||||
gui.suggestionsAsyncHandler.Do(func() func() {
|
||||
// assuming here that the confirmation view is what we're typing into.
|
||||
// This assumption may prove false over time
|
||||
suggestions := gui.findSuggestions(gui.Views.Confirmation.TextArea.GetContent())
|
||||
return func() { gui.setSuggestions(suggestions) }
|
||||
})
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return func(input string) []*types.Suggestion {
|
||||
matchingNames := []string{}
|
||||
_ = gui.State.FilesTrie.VisitFuzzy(patricia.Prefix(input), true, func(prefix patricia.Prefix, item patricia.Item, skipped int) error {
|
||||
matchingNames = append(matchingNames, item.(string))
|
||||
return nil
|
||||
})
|
||||
|
||||
// doing another fuzzy search for good measure
|
||||
matchingNames = utils.FuzzySearch(input, matchingNames)
|
||||
|
||||
suggestions := make([]*types.Suggestion, len(matchingNames))
|
||||
for i, name := range matchingNames {
|
||||
suggestions[i] = &types.Suggestion{
|
||||
Value: name,
|
||||
Label: name,
|
||||
}
|
||||
}
|
||||
|
||||
return suggestions
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) getRemoteBranchNames(separator string) []string {
|
||||
result := []string{}
|
||||
for _, remote := range gui.State.Remotes {
|
||||
for _, branch := range remote.Branches {
|
||||
result = append(result, fmt.Sprintf("%s%s%s", remote.Name, separator, branch.Name))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (gui *Gui) getRemoteBranchesSuggestionsFunc(separator string) func(string) []*types.Suggestion {
|
||||
return fuzzySearchFunc(gui.getRemoteBranchNames(separator))
|
||||
}
|
||||
|
||||
func (gui *Gui) getTagNames() []string {
|
||||
result := make([]string, len(gui.State.Tags))
|
||||
for i, tag := range gui.State.Tags {
|
||||
result[i] = tag.Name
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (gui *Gui) getRefsSuggestionsFunc() func(string) []*types.Suggestion {
|
||||
remoteBranchNames := gui.getRemoteBranchNames("/")
|
||||
localBranchNames := gui.getBranchNames()
|
||||
tagNames := gui.getTagNames()
|
||||
additionalRefNames := []string{"HEAD", "FETCH_HEAD", "MERGE_HEAD", "ORIG_HEAD"}
|
||||
|
||||
refNames := append(append(append(remoteBranchNames, localBranchNames...), tagNames...), additionalRefNames...)
|
||||
|
||||
return fuzzySearchFunc(refNames)
|
||||
}
|
||||
|
||||
func (gui *Gui) getCustomCommandsHistorySuggestionsFunc() func(string) []*types.Suggestion {
|
||||
// reversing so that we display the latest command first
|
||||
history := utils.Reverse(gui.Config.GetAppState().CustomCommandsHistory)
|
||||
|
||||
return fuzzySearchFunc(history)
|
||||
}
|
||||
|
||||
func fuzzySearchFunc(options []string) func(string) []*types.Suggestion {
|
||||
return func(input string) []*types.Suggestion {
|
||||
var matches []string
|
||||
if input == "" {
|
||||
matches = options
|
||||
} else {
|
||||
matches = utils.FuzzySearch(input, options)
|
||||
}
|
||||
|
||||
return matchesToSuggestions(matches)
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
const HORIZONTAL_SCROLL_FACTOR = 3
|
||||
|
||||
// these views need to be re-rendered when the screen mode changes. The commits view,
|
||||
// for example, will show authorship information in half and full screen mode.
|
||||
func (gui *Gui) rerenderViewsWithScreenModeDependentContent() error {
|
||||
@@ -114,7 +116,7 @@ func (gui *Gui) linesToScrollDown(view *gocui.View) int {
|
||||
|
||||
func (gui *Gui) scrollUpMain() error {
|
||||
if gui.canScrollMergePanel() {
|
||||
gui.State.Panels.Merging.UserScrolling = true
|
||||
gui.State.Panels.Merging.UserVerticalScrolling = true
|
||||
}
|
||||
|
||||
return gui.scrollUpView(gui.Views.Main)
|
||||
@@ -122,12 +124,33 @@ func (gui *Gui) scrollUpMain() error {
|
||||
|
||||
func (gui *Gui) scrollDownMain() error {
|
||||
if gui.canScrollMergePanel() {
|
||||
gui.State.Panels.Merging.UserScrolling = true
|
||||
gui.State.Panels.Merging.UserVerticalScrolling = true
|
||||
}
|
||||
|
||||
return gui.scrollDownView(gui.Views.Main)
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollLeftMain() error {
|
||||
gui.scrollLeft(gui.Views.Main)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollRightMain() error {
|
||||
gui.scrollRight(gui.Views.Main)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollLeft(view *gocui.View) {
|
||||
newOriginX := utils.Max(view.OriginX()-view.InnerWidth()/HORIZONTAL_SCROLL_FACTOR, 0)
|
||||
_ = view.SetOriginX(newOriginX)
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollRight(view *gocui.View) {
|
||||
_ = view.SetOriginX(view.OriginX() + view.InnerWidth()/HORIZONTAL_SCROLL_FACTOR)
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollUpSecondary() error {
|
||||
return gui.scrollUpView(gui.Views.Secondary)
|
||||
}
|
||||
|
||||
+37
-16
@@ -1,5 +1,11 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
)
|
||||
|
||||
// Currently there is a bug where if we switch to a subprocess from within
|
||||
// WithWaitingStatus we get stuck there and can't return to lazygit. We could
|
||||
// fix this bug, or just stop running subprocesses from within there, given that
|
||||
@@ -19,23 +25,38 @@ func (gui *Gui) withGpgHandling(cmdStr string, waitingStatus string, onSuccess f
|
||||
return err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
} else {
|
||||
return gui.WithWaitingStatus(waitingStatus, func() error {
|
||||
err := gui.OSCommand.RunCommand(cmdStr)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if onSuccess != nil {
|
||||
if err := onSuccess(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
})
|
||||
return gui.RunAndStream(cmdStr, waitingStatus, onSuccess)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
func (gui *Gui) RunAndStream(cmdStr string, waitingStatus string, onSuccess func() error) error {
|
||||
return gui.WithWaitingStatus(waitingStatus, func() error {
|
||||
cmd := gui.OSCommand.ShellCommandFromString(cmdStr)
|
||||
cmd.Env = append(cmd.Env, "TERM=dumb")
|
||||
cmdWriter := gui.getCmdWriter()
|
||||
cmd.Stdout = cmdWriter
|
||||
cmd.Stderr = cmdWriter
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
if _, err := cmd.Stdout.Write([]byte(fmt.Sprintf("%s\n", style.FgRed.Sprint(err.Error())))); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
return gui.surfaceError(
|
||||
fmt.Errorf(
|
||||
gui.Tr.GitCommandFailed, gui.Config.GetUserConfig().Keybinding.Universal.ExtrasMenu,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if onSuccess != nil {
|
||||
if err := onSuccess(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
})
|
||||
}
|
||||
|
||||
+54
-58
@@ -5,14 +5,12 @@ import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
@@ -21,15 +19,20 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/lbl"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/modes/cherrypicking"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/modes/diffing"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/modes/filtering"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation/authors"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation/graph"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/tasks"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/updates"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/mattn/go-runewidth"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/ozeidan/fuzzy-patricia.v3/patricia"
|
||||
)
|
||||
|
||||
// screen sizing determines how much space your selected window takes up (window
|
||||
@@ -49,10 +52,6 @@ const StartupPopupVersion = 5
|
||||
// OverlappingEdges determines if panel edges overlap
|
||||
var OverlappingEdges = false
|
||||
|
||||
func init() {
|
||||
runewidth.DefaultCondition.EastAsianWidth = false
|
||||
}
|
||||
|
||||
type ContextManager struct {
|
||||
ContextStack []Context
|
||||
sync.RWMutex
|
||||
@@ -120,6 +119,8 @@ type Gui struct {
|
||||
|
||||
// the extras window contains things like the command log
|
||||
ShowExtrasWindow bool
|
||||
|
||||
suggestionsAsyncHandler *tasks.AsyncHandler
|
||||
}
|
||||
|
||||
type listPanelState struct {
|
||||
@@ -145,9 +146,9 @@ type LblPanelState struct {
|
||||
type MergingPanelState struct {
|
||||
*mergeconflicts.State
|
||||
|
||||
// UserScrolling tells us if the user has started scrolling through the file themselves
|
||||
// UserVerticalScrolling tells us if the user has started scrolling through the file themselves
|
||||
// in which case we won't auto-scroll to a conflict.
|
||||
UserScrolling bool
|
||||
UserVerticalScrolling bool
|
||||
}
|
||||
|
||||
type filePanelState struct {
|
||||
@@ -269,31 +270,10 @@ const (
|
||||
COMPLETE
|
||||
)
|
||||
|
||||
// if ref is blank we're not diffing anything
|
||||
type Diffing struct {
|
||||
Ref string
|
||||
Reverse bool
|
||||
}
|
||||
|
||||
func (m *Diffing) Active() bool {
|
||||
return m.Ref != ""
|
||||
}
|
||||
|
||||
type CherryPicking struct {
|
||||
CherryPickedCommits []*models.Commit
|
||||
|
||||
// we only allow cherry picking from one context at a time, so you can't copy a commit from the local commits context and then also copy a commit in the reflog context
|
||||
ContextKey ContextKey
|
||||
}
|
||||
|
||||
func (m *CherryPicking) Active() bool {
|
||||
return len(m.CherryPickedCommits) > 0
|
||||
}
|
||||
|
||||
type Modes struct {
|
||||
Filtering filtering.Filtering
|
||||
CherryPicking CherryPicking
|
||||
Diffing Diffing
|
||||
CherryPicking cherrypicking.CherryPicking
|
||||
Diffing diffing.Diffing
|
||||
}
|
||||
|
||||
type guiMutexes struct {
|
||||
@@ -335,6 +315,8 @@ type guiState struct {
|
||||
RetainOriginalDir bool
|
||||
IsRefreshingFiles bool
|
||||
Searching searchingState
|
||||
// if this is true, we'll load our commits using `git log --all`
|
||||
ShowWholeGitGraph bool
|
||||
ScreenMode WindowMaximisation
|
||||
Ptmx *os.File
|
||||
PrevMainWidth int
|
||||
@@ -358,6 +340,12 @@ type guiState struct {
|
||||
// do this whenever we switch back and forth between repos to get the views
|
||||
// back in sync with the repo state
|
||||
ViewsSetup bool
|
||||
|
||||
// flag as to whether or not the diff view should ignore whitespace
|
||||
IgnoreWhitespaceInDiffView bool
|
||||
|
||||
// for displaying suggestions while typing in a file name
|
||||
FilesTrie *patricia.Trie
|
||||
}
|
||||
|
||||
// reuseState determines if we pull the repo state from our repo state map or
|
||||
@@ -410,7 +398,7 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
|
||||
Remotes: &remotePanelState{listPanelState{SelectedLineIdx: 0}},
|
||||
RemoteBranches: &remoteBranchesState{listPanelState{SelectedLineIdx: -1}},
|
||||
Tags: &tagsPanelState{listPanelState{SelectedLineIdx: -1}},
|
||||
Commits: &commitPanelState{listPanelState: listPanelState{SelectedLineIdx: -1}, LimitCommits: true},
|
||||
Commits: &commitPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}, LimitCommits: true},
|
||||
ReflogCommits: &reflogCommitPanelState{listPanelState{SelectedLineIdx: 0}},
|
||||
SubCommits: &subCommitPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}, refName: ""},
|
||||
CommitFiles: &commitFilesPanelState{listPanelState: listPanelState{SelectedLineIdx: -1}, refName: ""},
|
||||
@@ -418,18 +406,15 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
|
||||
Menu: &menuPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}, OnPress: nil},
|
||||
Suggestions: &suggestionsPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}},
|
||||
Merging: &MergingPanelState{
|
||||
State: mergeconflicts.NewState(),
|
||||
UserScrolling: false,
|
||||
State: mergeconflicts.NewState(),
|
||||
UserVerticalScrolling: false,
|
||||
},
|
||||
},
|
||||
Ptmx: nil,
|
||||
Modes: Modes{
|
||||
Filtering: filtering.NewFiltering(filterPath),
|
||||
CherryPicking: CherryPicking{
|
||||
CherryPickedCommits: make([]*models.Commit, 0),
|
||||
ContextKey: "",
|
||||
},
|
||||
Diffing: Diffing{},
|
||||
Filtering: filtering.New(filterPath),
|
||||
CherryPicking: cherrypicking.New(),
|
||||
Diffing: diffing.New(),
|
||||
},
|
||||
ViewContextMap: contexts.initialViewContextMap(),
|
||||
ViewTabContextMap: contexts.initialViewTabContextMap(),
|
||||
@@ -437,6 +422,7 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
|
||||
// TODO: put contexts in the context manager
|
||||
ContextManager: NewContextManager(initialContext),
|
||||
Contexts: contexts,
|
||||
FilesTrie: patricia.NewTrie(),
|
||||
}
|
||||
|
||||
gui.RepoStateMap[Repo(currentDir)] = gui.State
|
||||
@@ -446,19 +432,20 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
|
||||
// NewGui builds a new gui handler
|
||||
func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *oscommands.OSCommand, tr *i18n.TranslationSet, config config.AppConfigurer, updater *updates.Updater, filterPath string, showRecentRepos bool) (*Gui, error) {
|
||||
gui := &Gui{
|
||||
Log: log,
|
||||
GitCommand: gitCommand,
|
||||
OSCommand: oSCommand,
|
||||
Config: config,
|
||||
Tr: tr,
|
||||
Updater: updater,
|
||||
statusManager: &statusManager{},
|
||||
viewBufferManagerMap: map[string]*tasks.ViewBufferManager{},
|
||||
showRecentRepos: showRecentRepos,
|
||||
RepoPathStack: []string{},
|
||||
RepoStateMap: map[Repo]*guiState{},
|
||||
CmdLog: []string{},
|
||||
ShowExtrasWindow: config.GetUserConfig().Gui.ShowCommandLog,
|
||||
Log: log,
|
||||
GitCommand: gitCommand,
|
||||
OSCommand: oSCommand,
|
||||
Config: config,
|
||||
Tr: tr,
|
||||
Updater: updater,
|
||||
statusManager: &statusManager{},
|
||||
viewBufferManagerMap: map[string]*tasks.ViewBufferManager{},
|
||||
showRecentRepos: showRecentRepos,
|
||||
RepoPathStack: []string{},
|
||||
RepoStateMap: map[Repo]*guiState{},
|
||||
CmdLog: []string{},
|
||||
ShowExtrasWindow: config.ShowCommandLogOnStartup(),
|
||||
suggestionsAsyncHandler: tasks.NewAsyncHandler(),
|
||||
}
|
||||
|
||||
gui.resetState(filterPath, false)
|
||||
@@ -469,9 +456,17 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *oscom
|
||||
oSCommand.SetOnRunCommand(onRunCommand)
|
||||
gui.OnRunCommand = onRunCommand
|
||||
|
||||
authors.SetCustomAuthors(gui.Config.GetUserConfig().Gui.AuthorColors)
|
||||
|
||||
return gui, nil
|
||||
}
|
||||
|
||||
var RuneReplacements = map[rune]string{
|
||||
// for the commit graph
|
||||
graph.MergeSymbol: "M",
|
||||
graph.CommitSymbol: "o",
|
||||
}
|
||||
|
||||
// Run setup the gui with keybindings and start the mainloop
|
||||
func (gui *Gui) Run() error {
|
||||
recordEvents := recordingEvents()
|
||||
@@ -482,7 +477,7 @@ func (gui *Gui) Run() error {
|
||||
playMode = gocui.REPLAYING
|
||||
}
|
||||
|
||||
g, err := gocui.NewGui(gocui.OutputTrue, OverlappingEdges, playMode, headless())
|
||||
g, err := gocui.NewGui(gocui.OutputTrue, OverlappingEdges, playMode, headless(), RuneReplacements)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -516,7 +511,7 @@ func (gui *Gui) Run() error {
|
||||
g.NextSearchMatchKey = gui.getKey(userConfig.Keybinding.Universal.NextMatch)
|
||||
g.PrevSearchMatchKey = gui.getKey(userConfig.Keybinding.Universal.PrevMatch)
|
||||
|
||||
g.ASCII = runtime.GOOS == "windows" && runewidth.IsEastAsian()
|
||||
g.ShowListFooter = userConfig.Gui.ShowListFooter
|
||||
|
||||
if userConfig.Gui.MouseEvents {
|
||||
g.Mouse = true
|
||||
@@ -628,7 +623,7 @@ func (gui *Gui) runSubprocess(subprocess *exec.Cmd) error {
|
||||
subprocess.Stderr = os.Stdout
|
||||
subprocess.Stdin = os.Stdin
|
||||
|
||||
fmt.Fprintf(os.Stdout, "\n%s\n\n", utils.ColoredString("+ "+strings.Join(subprocess.Args, " "), color.FgBlue))
|
||||
fmt.Fprintf(os.Stdout, "\n%s\n\n", style.FgBlue.Sprint("+ "+strings.Join(subprocess.Args, " ")))
|
||||
|
||||
if err := subprocess.Run(); err != nil {
|
||||
// not handling the error explicitly because usually we're going to see it
|
||||
@@ -640,7 +635,7 @@ func (gui *Gui) runSubprocess(subprocess *exec.Cmd) error {
|
||||
subprocess.Stderr = ioutil.Discard
|
||||
subprocess.Stdin = nil
|
||||
|
||||
fmt.Fprintf(os.Stdout, "\n%s", utils.ColoredString(gui.Tr.PressEnterToReturn, color.FgGreen))
|
||||
fmt.Fprintf(os.Stdout, "\n%s", style.FgGreen.Sprint(gui.Tr.PressEnterToReturn))
|
||||
fmt.Scanln() // wait for enter press
|
||||
|
||||
return nil
|
||||
@@ -726,6 +721,7 @@ func (gui *Gui) startBackgroundFetch() {
|
||||
} else {
|
||||
gui.goEvery(time.Second*time.Duration(userConfig.Refresher.FetchInterval), gui.stopChan, func() error {
|
||||
err := gui.fetch(false, "")
|
||||
gui.render()
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user