Compare commits
349 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9548b5d00 | ||
|
|
e3da89efb7 | ||
|
|
19d2994af8 | ||
|
|
ad4fd67db3 | ||
|
|
5f36eb507a | ||
|
|
1caec9114c | ||
|
|
e3af0ed43a | ||
|
|
18c39c5f24 | ||
|
|
07afb5359e | ||
|
|
3f1cda88ed | ||
|
|
edd43bcbeb | ||
|
|
061db91002 | ||
|
|
2455faec1b | ||
|
|
6fddf2aa8b | ||
|
|
c24bb11141 | ||
|
|
0eea75e8c6 | ||
|
|
b21997d6b4 | ||
|
|
e94e8fc5b6 | ||
|
|
d8084cd558 | ||
|
|
65f910ebd8 | ||
|
|
26c07b1ab3 | ||
|
|
3d4470a6c0 | ||
|
|
053a66a7be | ||
|
|
985fe482e8 | ||
|
|
4ab4af441e | ||
|
|
bdc54a5deb | ||
|
|
d913c04109 | ||
|
|
9ef65574db | ||
|
|
f89747451a | ||
|
|
8a76b5a4ee | ||
|
|
1a7d0cd7ae | ||
|
|
2696a63a0a | ||
|
|
8c8b925b3a | ||
|
|
b8735cc609 | ||
|
|
517dab7d05 | ||
|
|
e5f0301c66 | ||
|
|
eff6c4283b | ||
|
|
7888ff6cb9 | ||
|
|
e7a005f44d | ||
|
|
3e58797096 | ||
|
|
b1d6ccddfb | ||
|
|
4df003cc44 | ||
|
|
d9db5ccfbe | ||
|
|
7bc8b96aba | ||
|
|
38743ec99f | ||
|
|
630de34bf2 | ||
|
|
fca2f90f57 | ||
|
|
fdf0d4a2c3 | ||
|
|
b4ea565c99 | ||
|
|
76e6745526 | ||
|
|
3771f9c98b | ||
|
|
4f627e5d27 | ||
|
|
089e3bf4fe | ||
|
|
d5c1cfda4b | ||
|
|
3926d0f278 | ||
|
|
18283ad41b | ||
|
|
1996eddd91 | ||
|
|
de0e885c65 | ||
|
|
f7ffbbd72a | ||
|
|
0fbde05928 | ||
|
|
ba844c18a5 | ||
|
|
e1cf6912db | ||
|
|
c99d373e13 | ||
|
|
ecfafb6fbe | ||
|
|
14d9e776be | ||
|
|
ca88620e8f | ||
|
|
9feaf5d70f | ||
|
|
3e3151f86a | ||
|
|
28cdcddb0a | ||
|
|
02bf6a5c17 | ||
|
|
8abc953582 | ||
|
|
9c4f837d45 | ||
|
|
2f45db8f7c | ||
|
|
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 |
9
.github/dependabot.yml
vendored
Normal file
9
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
allowed_updates:
|
||||
- match:
|
||||
update_type: "security"
|
||||
23
.github/workflows/cd.yml
vendored
23
.github/workflows/cd.yml
vendored
@@ -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
|
||||
|
||||
49
.github/workflows/ci.yml
vendored
49
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
6
.github/workflows/lint.yml
vendored
6
.github/workflows/lint.yml
vendored
@@ -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
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
15
README.md
15
README.md
@@ -27,6 +27,7 @@ If you're a mere mortal like me and you're tired of hearing how powerful git is
|
||||
- [Arch Linux](#arch-linux)
|
||||
- [Fedora and CentOS 7](#fedora-and-centos-7)
|
||||
- [Solus Linux](#solus-linux)
|
||||
- [Funtoo Linux](#funtoo-linux)
|
||||
- [FreeBSD](#freebsd)
|
||||
- [Conda](#conda)
|
||||
- [Go](#go)
|
||||
@@ -53,7 +54,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 +83,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
|
||||
@@ -140,6 +143,14 @@ sudo dnf install lazygit
|
||||
sudo eopkg install lazygit
|
||||
```
|
||||
|
||||
### Funtoo Linux
|
||||
|
||||
Funtoo Linux has an autogenerated lazygit package in [dev-kit](https://github.com/funtoo/dev-kit/tree/1.4-release/dev-vcs/lazygit):
|
||||
|
||||
```sh
|
||||
sudo emerge dev-vcs/lazygit
|
||||
```
|
||||
|
||||
### FreeBSD
|
||||
|
||||
```sh
|
||||
@@ -255,7 +266,7 @@ See the [docs](docs/Custom_Command_Keybindings.md)
|
||||
- Easily check out recent branches
|
||||
- Scroll through logs/diffs of branches/commits/stash
|
||||
- Quick pushing/pulling
|
||||
- Squash down and rename commits
|
||||
- Squash down and reword commits
|
||||
|
||||
### Resolving merge conflicts
|
||||
|
||||
|
||||
5
bump_gocui.sh
Executable file
5
bump_gocui.sh
Executable file
@@ -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)
|
||||
123
docs/Config.md
123
docs/Config.md
@@ -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,12 +36,17 @@ 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
|
||||
showFileTree: true # 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
|
||||
@@ -48,19 +54,33 @@ git:
|
||||
paging:
|
||||
colorArg: always
|
||||
useConfig: false
|
||||
commit:
|
||||
signOff: false
|
||||
merging:
|
||||
# only applicable to unix users
|
||||
manualCommit: false
|
||||
# extra args passed to `git merge`, e.g. --no-ff
|
||||
args: ''
|
||||
pull:
|
||||
mode: 'merge' # one of 'merge' | 'rebase' | 'ff-only'
|
||||
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
|
||||
diffContextSize: 3 # how many lines of context are shown around a change in diffs
|
||||
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 +108,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 +151,11 @@ keybinding:
|
||||
diffingMenu-alt: '<c-e>' # deprecated
|
||||
copyToClipboard: '<c-o>'
|
||||
submitEditorText: '<enter>'
|
||||
appendNewline: '<tab>'
|
||||
appendNewline: '<a-enter>'
|
||||
extrasMenu: '@'
|
||||
toggleWhitespaceInDiffView: '<c-w>'
|
||||
increaseContextInDiffView: '}'
|
||||
decreaseContextInDiffView: '{'
|
||||
status:
|
||||
checkForUpdate: 'u'
|
||||
recentRepos: '<enter>'
|
||||
@@ -146,6 +174,7 @@ keybinding:
|
||||
toggleTreeView: '`'
|
||||
branches:
|
||||
createPullRequest: 'o'
|
||||
viewPullRequestOptions: 'O'
|
||||
checkoutBranchByName: 'c'
|
||||
forceCheckoutBranch: 'F'
|
||||
rebaseBranch: 'r'
|
||||
@@ -175,6 +204,7 @@ keybinding:
|
||||
checkoutCommit: '<space>'
|
||||
resetCherryPick: '<c-R>'
|
||||
copyCommitMessageToClipboard: '<c-y>'
|
||||
openLogMenu: '<c-l>'
|
||||
stash:
|
||||
popStash: 'g'
|
||||
commitFiles:
|
||||
@@ -196,14 +226,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 +243,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 +308,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 +318,12 @@ The available attributes are:
|
||||
- magenta
|
||||
- cyan
|
||||
- white
|
||||
- '#ff00ff'
|
||||
|
||||
**Modifiers**
|
||||
|
||||
- bold
|
||||
- default
|
||||
- reverse # useful for high-contrast
|
||||
- underline
|
||||
|
||||
@@ -289,6 +376,28 @@ Alternatively you may have bold fonts disabled in your terminal, in which case e
|
||||
|
||||
If you're still having trouble please raise an issue.
|
||||
|
||||
## Custom Author Color
|
||||
|
||||
Lazygit will assign a random color for every commit author in the commits pane by default.
|
||||
|
||||
You can customize the color in case you're not happy with the randomly assigned one:
|
||||
|
||||
```yaml
|
||||
gui:
|
||||
authorColors:
|
||||
'John Smith': '#ff0000' # use red for John Smith
|
||||
```
|
||||
|
||||
You can use wildcard to set a unified color in case your are lazy to customize the color for every author or you just want a single color for all/other authors:
|
||||
```yaml
|
||||
gui:
|
||||
authorColors:
|
||||
# use red for John Smith
|
||||
'John Smith': '#ff0000'
|
||||
# use blue for other authors
|
||||
'*': '#0000ff'
|
||||
```
|
||||
|
||||
## Example Coloring
|
||||
|
||||

|
||||
|
||||
@@ -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_ |
|
||||
|
||||
@@ -33,29 +33,31 @@ git commit -am "myfile1"
|
||||
|
||||
## Running tests
|
||||
|
||||
To run all tests
|
||||
To run all tests - assuming you're at the project root:
|
||||
```
|
||||
go test pkg/gui/gui_test.go
|
||||
go test ./pkg/gui/
|
||||
```
|
||||
|
||||
To run them in parallel
|
||||
```
|
||||
PARALLEL=true go test pkg/gui/gui_test.go
|
||||
PARALLEL=true go test ./pkg/gui
|
||||
```
|
||||
|
||||
To run a single test
|
||||
```
|
||||
go test pkg/gui/gui_test.go -run /<test name>
|
||||
go test ./pkg/gui -run /<test name>
|
||||
# For example, to run the `tags` test:
|
||||
go test ./pkg/gui -run /tags
|
||||
```
|
||||
|
||||
To run a test at a certain speed
|
||||
```
|
||||
SPEED=2 go test pkg/gui/gui_test.go -run /<test name>
|
||||
SPEED=2 go test ./pkg/gui -run /<test name>
|
||||
```
|
||||
|
||||
To update a snapshot
|
||||
```
|
||||
UPDATE_SNAPSHOTS=true go test pkg/gui/gui_test.go -run /<test name>
|
||||
UPDATE_SNAPSHOTS=true go test ./pkg/gui -run /<test name>
|
||||
```
|
||||
|
||||
## Creating a new test
|
||||
@@ -66,7 +68,7 @@ To create a new test:
|
||||
3) If you want to have a config folder for just that test, create a `config` directory to contain a `config.yml` and optionally a `state.yml` file. Otherwise, the `test/default_test_config` directory will be used.
|
||||
4) From the lazygit root directory, run:
|
||||
```
|
||||
RECORD_EVENTS=true go test pkg/gui/gui_test.go -run /<test name>
|
||||
RECORD_EVENTS=true go test ./pkg/gui -run /<test name>
|
||||
```
|
||||
5) Feel free to re-attempt recording as many times as you like. In the absence of a proper testing framework, the more deliberate your keypresses, the better!
|
||||
6) Once satisfied with the recording, stage all the newly created files: `test.json`, `setup.sh`, `recording.json` and the `expected` directory that contains a copy of the repo you created.
|
||||
|
||||
@@ -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
|
||||
@@ -118,11 +121,11 @@
|
||||
<pre>
|
||||
<kbd>s</kbd>: squash down
|
||||
<kbd>r</kbd>: reword commit
|
||||
<kbd>R</kbd>: rename commit with editor
|
||||
<kbd>R</kbd>: reword commit with editor
|
||||
<kbd>g</kbd>: reset to this commit
|
||||
<kbd>f</kbd>: fixup commit
|
||||
<kbd>F</kbd>: create fixup commit for this commit
|
||||
<kbd>S</kbd>: squash above commits
|
||||
<kbd>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>
|
||||
|
||||
|
||||
32
go.mod
32
go.mod
@@ -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
|
||||
|
||||
151
go.sum
151
go.sum
@@ -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=
|
||||
|
||||
7
main.go
7
main.go
@@ -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)
|
||||
}
|
||||
|
||||
@@ -15,26 +15,25 @@ 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/common"
|
||||
"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"
|
||||
)
|
||||
|
||||
// App struct
|
||||
type App struct {
|
||||
closers []io.Closer
|
||||
|
||||
*common.Common
|
||||
closers []io.Closer
|
||||
Config config.AppConfigurer
|
||||
Log *logrus.Entry
|
||||
OSCommand *oscommands.OSCommand
|
||||
GitCommand *commands.GitCommand
|
||||
Gui *gui.Gui
|
||||
Tr *i18n.TranslationSet
|
||||
Updater *updates.Updater // may only need this on the Gui
|
||||
ClientContext string
|
||||
}
|
||||
@@ -97,14 +96,25 @@ func newLogger(config config.AppConfigurer) *logrus.Entry {
|
||||
|
||||
// NewApp bootstrap a new application
|
||||
func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
|
||||
userConfig := config.GetUserConfig()
|
||||
|
||||
app := &App{
|
||||
closers: []io.Closer{},
|
||||
Config: config,
|
||||
}
|
||||
var err error
|
||||
app.Log = newLogger(config)
|
||||
app.Tr = i18n.NewTranslationSet(app.Log)
|
||||
log := newLogger(config)
|
||||
tr, err := i18n.NewTranslationSetFromConfig(log, userConfig.Gui.Language)
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
|
||||
app.Common = &common.Common{
|
||||
Log: log,
|
||||
Tr: tr,
|
||||
UserConfig: userConfig,
|
||||
Debug: config.GetDebug(),
|
||||
}
|
||||
|
||||
// if we are being called in 'demon' mode, we can just return here
|
||||
app.ClientContext = os.Getenv("LAZYGIT_CLIENT_COMMAND")
|
||||
@@ -112,9 +122,9 @@ func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
|
||||
return app, nil
|
||||
}
|
||||
|
||||
app.OSCommand = oscommands.NewOSCommand(app.Log, config)
|
||||
app.OSCommand = oscommands.NewOSCommand(app.Common)
|
||||
|
||||
app.Updater, err = updates.NewUpdater(app.Log, config, app.OSCommand, app.Tr)
|
||||
app.Updater, err = updates.NewUpdater(app.Common, config, app.OSCommand)
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
@@ -124,12 +134,16 @@ 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.Common,
|
||||
app.OSCommand,
|
||||
git_config.NewStdCachedGitConfig(app.Log),
|
||||
)
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
|
||||
app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config, app.Updater, filterPath, showRecentRepos)
|
||||
app.Gui, err = gui.NewGui(app.Common, app.GitCommand, app.OSCommand, config, app.Updater, filterPath, showRecentRepos)
|
||||
if err != nil {
|
||||
return app, err
|
||||
}
|
||||
@@ -137,7 +151,7 @@ func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
|
||||
}
|
||||
|
||||
func (app *App) validateGitVersion() error {
|
||||
output, err := app.OSCommand.RunCommandWithOutput("git --version")
|
||||
output, err := app.OSCommand.Cmd.New("git --version").RunWithOutput()
|
||||
// if we get an error anywhere here we'll show the same status
|
||||
minVersionError := errors.New(app.Tr.MinGitVersionError)
|
||||
if err != nil {
|
||||
@@ -194,7 +208,7 @@ func (app *App) setupRepo() (bool, error) {
|
||||
}
|
||||
|
||||
shouldInitRepo := true
|
||||
notARepository := app.Config.GetUserConfig().NotARepository
|
||||
notARepository := app.UserConfig.NotARepository
|
||||
if notARepository == "prompt" {
|
||||
// Offer to initialize a new repository in current directory.
|
||||
fmt.Print(app.Tr.CreateRepo)
|
||||
@@ -222,7 +236,7 @@ func (app *App) setupRepo() (bool, error) {
|
||||
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := app.OSCommand.RunCommand("git init"); err != nil {
|
||||
if err := app.OSCommand.Cmd.New("git init").Run(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
@@ -318,6 +332,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 +343,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)
|
||||
}
|
||||
|
||||
30
pkg/app/logging.go
Normal file
30
pkg/app/logging.go
Normal file
@@ -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)
|
||||
}
|
||||
72
pkg/app/logging_windows.go
Normal file
72
pkg/app/logging_windows.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -11,19 +11,19 @@ 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.Cmd.New(fmt.Sprintf("git checkout -b %s %s", c.OSCommand.Quote(name), c.OSCommand.Quote(base))).Run()
|
||||
}
|
||||
|
||||
// CurrentBranchName get the current branch name and displayname.
|
||||
// the first returned string is the name and the second is the displayname
|
||||
// e.g. name is 123asdf and displayname is '(HEAD detached at 123asdf)'
|
||||
func (c *GitCommand) CurrentBranchName() (string, string, error) {
|
||||
branchName, err := c.RunCommandWithOutput("git symbolic-ref --short HEAD")
|
||||
branchName, err := c.Cmd.New("git symbolic-ref --short HEAD").RunWithOutput()
|
||||
if err == nil && branchName != "HEAD\n" {
|
||||
trimmedBranchName := strings.TrimSpace(branchName)
|
||||
return trimmedBranchName, trimmedBranchName, nil
|
||||
}
|
||||
output, err := c.RunCommandWithOutput("git branch --contains")
|
||||
output, err := c.Cmd.New("git branch --contains").RunWithOutput()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
@@ -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.Cmd.New(fmt.Sprintf("%s %s", command, c.OSCommand.Quote(branch))).Run()
|
||||
}
|
||||
|
||||
// Checkout checks out a branch (or commit), with --force if you set the force arg to true
|
||||
@@ -61,36 +61,41 @@ 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.Cmd.New(fmt.Sprintf("git checkout%s %s", forceArg, c.OSCommand.Quote(branch))).
|
||||
// prevents git from prompting us for input which would freeze the program
|
||||
// TODO: see if this is actually needed here
|
||||
AddEnvVars("GIT_TERMINAL_PROMPT=0").
|
||||
AddEnvVars(options.EnvVars...).
|
||||
Run()
|
||||
}
|
||||
|
||||
// GetBranchGraph gets the color-formatted graph of the log for the given branch
|
||||
// Currently it limits the result to 100 commits, but when we get async stuff
|
||||
// working we can do lazy loading
|
||||
func (c *GitCommand) GetBranchGraph(branchName string) (string, error) {
|
||||
cmdStr := c.GetBranchGraphCmdStr(branchName)
|
||||
return c.OSCommand.RunCommandWithOutput(cmdStr)
|
||||
return c.GetBranchGraphCmdObj(branchName).RunWithOutput()
|
||||
}
|
||||
|
||||
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.Cmd.New(fmt.Sprintf("git rev-parse --abbrev-ref --symbolic-full-name %s@{u}", c.OSCommand.Quote(branchName))).RunWithOutput()
|
||||
return strings.TrimSpace(output), err
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetBranchGraphCmdStr(branchName string) string {
|
||||
branchLogCmdTemplate := c.Config.GetUserConfig().Git.BranchLogCmd
|
||||
func (c *GitCommand) GetBranchGraphCmdObj(branchName string) oscommands.ICmdObj {
|
||||
branchLogCmdTemplate := c.UserConfig.Git.BranchLogCmd
|
||||
templateValues := map[string]string{
|
||||
"branchName": branchName,
|
||||
"branchName": c.OSCommand.Quote(branchName),
|
||||
}
|
||||
return utils.ResolvePlaceholderString(branchLogCmdTemplate, templateValues)
|
||||
return c.Cmd.New(utils.ResolvePlaceholderString(branchLogCmdTemplate, templateValues))
|
||||
}
|
||||
|
||||
func (c *GitCommand) SetUpstreamBranch(upstream string) error {
|
||||
return c.RunCommand("git branch -u %s", upstream)
|
||||
return c.Cmd.New("git branch -u " + c.OSCommand.Quote(upstream)).Run()
|
||||
}
|
||||
|
||||
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.Cmd.New(fmt.Sprintf("git branch --set-upstream-to=%s/%s %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(remoteBranchName), c.OSCommand.Quote(branchName))).Run()
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetCurrentBranchUpstreamDifferenceCount() (string, string) {
|
||||
@@ -105,11 +110,11 @@ func (c *GitCommand) GetBranchUpstreamDifferenceCount(branchName string) (string
|
||||
// current branch
|
||||
func (c *GitCommand) GetCommitDifferences(from, to string) (string, string) {
|
||||
command := "git rev-list %s..%s --count"
|
||||
pushableCount, err := c.OSCommand.RunCommandWithOutput(command, to, from)
|
||||
pushableCount, err := c.Cmd.New(fmt.Sprintf(command, to, from)).RunWithOutput()
|
||||
if err != nil {
|
||||
return "?", "?"
|
||||
}
|
||||
pullableCount, err := c.OSCommand.RunCommandWithOutput(command, from, to)
|
||||
pullableCount, err := c.Cmd.New(fmt.Sprintf(command, from, to)).RunWithOutput()
|
||||
if err != nil {
|
||||
return "?", "?"
|
||||
}
|
||||
@@ -122,40 +127,47 @@ type MergeOpts struct {
|
||||
|
||||
// Merge merge
|
||||
func (c *GitCommand) Merge(branchName string, opts MergeOpts) error {
|
||||
mergeArgs := c.Config.GetUserConfig().Git.Merging.Args
|
||||
mergeArg := ""
|
||||
if c.UserConfig.Git.Merging.Args != "" {
|
||||
mergeArg = " " + c.UserConfig.Git.Merging.Args
|
||||
}
|
||||
|
||||
command := fmt.Sprintf("git merge --no-edit %s %s", mergeArgs, branchName)
|
||||
command := fmt.Sprintf("git merge --no-edit%s %s", mergeArg, c.OSCommand.Quote(branchName))
|
||||
if opts.FastForwardOnly {
|
||||
command = fmt.Sprintf("%s --ff-only", command)
|
||||
}
|
||||
|
||||
return c.OSCommand.RunCommand(command)
|
||||
return c.OSCommand.Cmd.New(command).Run()
|
||||
}
|
||||
|
||||
// AbortMerge abort merge
|
||||
func (c *GitCommand) AbortMerge() error {
|
||||
return c.RunCommand("git merge --abort")
|
||||
return c.Cmd.New("git merge --abort").Run()
|
||||
}
|
||||
|
||||
func (c *GitCommand) IsHeadDetached() bool {
|
||||
err := c.RunCommand("git symbolic-ref -q HEAD")
|
||||
err := c.Cmd.New("git symbolic-ref -q HEAD").Run()
|
||||
return err != nil
|
||||
}
|
||||
|
||||
// ResetHardHead runs `git reset --hard`
|
||||
func (c *GitCommand) ResetHard(ref string) error {
|
||||
return c.RunCommand("git reset --hard " + ref)
|
||||
return c.Cmd.New("git reset --hard " + c.OSCommand.Quote(ref)).Run()
|
||||
}
|
||||
|
||||
// ResetSoft runs `git reset --soft HEAD`
|
||||
func (c *GitCommand) ResetSoft(ref string) error {
|
||||
return c.RunCommand("git reset --soft " + ref)
|
||||
return c.Cmd.New("git reset --soft " + c.OSCommand.Quote(ref)).Run()
|
||||
}
|
||||
|
||||
func (c *GitCommand) ResetMixed(ref string) error {
|
||||
return c.RunCommand("git reset --mixed " + ref)
|
||||
return c.Cmd.New("git reset --mixed " + c.OSCommand.Quote(ref)).Run()
|
||||
}
|
||||
|
||||
func (c *GitCommand) RenameBranch(oldName string, newName string) error {
|
||||
return c.RunCommand("git branch --move %s %s", oldName, newName)
|
||||
return c.Cmd.New(fmt.Sprintf("git branch --move %s %s", c.OSCommand.Quote(oldName), c.OSCommand.Quote(newName))).Run()
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetRawBranches() (string, error) {
|
||||
return c.Cmd.New(`git for-each-ref --sort=-committerdate --format="%(HEAD)|%(refname:short)|%(upstream:short)|%(upstream:track)" refs/heads`).RunWithOutput()
|
||||
}
|
||||
|
||||
@@ -1,120 +1,85 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/test"
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandGetCommitDifferences is a function.
|
||||
func TestGitCommandGetCommitDifferences(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(string, string)
|
||||
testName string
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
expectedPushables string
|
||||
expectedPullables string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Can't retrieve pushable count",
|
||||
func(string, ...string) *exec.Cmd {
|
||||
return secureexec.Command("test")
|
||||
},
|
||||
func(pushableCount string, pullableCount string) {
|
||||
assert.EqualValues(t, "?", pushableCount)
|
||||
assert.EqualValues(t, "?", pullableCount)
|
||||
},
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect("git rev-list @{u}..HEAD --count", "", errors.New("error")),
|
||||
"?", "?",
|
||||
},
|
||||
{
|
||||
"Can't retrieve pullable count",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
if args[1] == "HEAD..@{u}" {
|
||||
return secureexec.Command("test")
|
||||
}
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(pushableCount string, pullableCount string) {
|
||||
assert.EqualValues(t, "?", pushableCount)
|
||||
assert.EqualValues(t, "?", pullableCount)
|
||||
},
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect("git rev-list @{u}..HEAD --count", "1\n", nil).
|
||||
Expect("git rev-list HEAD..@{u} --count", "", errors.New("error")),
|
||||
"?", "?",
|
||||
},
|
||||
{
|
||||
"Retrieve pullable and pushable count",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
if args[1] == "HEAD..@{u}" {
|
||||
return secureexec.Command("echo", "10")
|
||||
}
|
||||
|
||||
return secureexec.Command("echo", "11")
|
||||
},
|
||||
func(pushableCount string, pullableCount string) {
|
||||
assert.EqualValues(t, "11", pushableCount)
|
||||
assert.EqualValues(t, "10", pullableCount)
|
||||
},
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect("git rev-list @{u}..HEAD --count", "1\n", nil).
|
||||
Expect("git rev-list HEAD..@{u} --count", "2\n", nil),
|
||||
"1", "2",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.GetCommitDifferences("HEAD", "@{u}"))
|
||||
gitCmd := NewDummyGitCommandWithRunner(s.runner)
|
||||
pushables, pullables := gitCmd.GetCommitDifferences("HEAD", "@{u}")
|
||||
assert.EqualValues(t, s.expectedPushables, pushables)
|
||||
assert.EqualValues(t, s.expectedPullables, pullables)
|
||||
s.runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandNewBranch is a function.
|
||||
func TestGitCommandNewBranch(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"checkout", "-b", "test", "master"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}
|
||||
runner := oscommands.NewFakeRunner(t).
|
||||
Expect(`git checkout -b "test" "master"`, "", nil)
|
||||
gitCmd := NewDummyGitCommandWithRunner(runner)
|
||||
|
||||
assert.NoError(t, gitCmd.NewBranch("test", "master"))
|
||||
runner.CheckForMissingCalls()
|
||||
}
|
||||
|
||||
// TestGitCommandDeleteBranch is a function.
|
||||
func TestGitCommandDeleteBranch(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
branch string
|
||||
force bool
|
||||
command func(string, ...string) *exec.Cmd
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Delete a branch",
|
||||
"test",
|
||||
false,
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"branch", "-d", "test"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
oscommands.NewFakeRunner(t).Expect(`git branch -d "test"`, "", nil),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Force delete a branch",
|
||||
"test",
|
||||
true,
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"branch", "-D", "test"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
oscommands.NewFakeRunner(t).Expect(`git branch -D "test"`, "", nil),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
@@ -123,31 +88,27 @@ func TestGitCommandDeleteBranch(t *testing.T) {
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.DeleteBranch(s.branch, s.force))
|
||||
gitCmd := NewDummyGitCommandWithRunner(s.runner)
|
||||
|
||||
s.test(gitCmd.DeleteBranch("test", s.force))
|
||||
s.runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandMerge is a function.
|
||||
func TestGitCommandMerge(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"merge", "--no-edit", "test"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}
|
||||
runner := oscommands.NewFakeRunner(t).
|
||||
Expect(`git merge --no-edit "test"`, "", nil)
|
||||
gitCmd := NewDummyGitCommandWithRunner(runner)
|
||||
|
||||
assert.NoError(t, gitCmd.Merge("test", MergeOpts{}))
|
||||
runner.CheckForMissingCalls()
|
||||
}
|
||||
|
||||
// TestGitCommandCheckout is a function.
|
||||
func TestGitCommandCheckout(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
test func(error)
|
||||
force bool
|
||||
}
|
||||
@@ -155,12 +116,7 @@ func TestGitCommandCheckout(t *testing.T) {
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Checkout",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"checkout", "test"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
oscommands.NewFakeRunner(t).Expect(`git checkout "test"`, "", nil),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
@@ -168,12 +124,7 @@ func TestGitCommandCheckout(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"Checkout forced",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"checkout", "--force", "test"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
oscommands.NewFakeRunner(t).Expect(`git checkout --force "test"`, "", nil),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
@@ -183,52 +134,43 @@ func TestGitCommandCheckout(t *testing.T) {
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
gitCmd := NewDummyGitCommandWithRunner(s.runner)
|
||||
s.test(gitCmd.Checkout("test", CheckoutOptions{Force: s.force}))
|
||||
s.runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandGetBranchGraph is a function.
|
||||
func TestGitCommandGetBranchGraph(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"log", "--graph", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium", "test", "--"}, args)
|
||||
return secureexec.Command("echo")
|
||||
}
|
||||
runner := oscommands.NewFakeRunner(t).ExpectArgs([]string{
|
||||
"git", "log", "--graph", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium", "test", "--",
|
||||
}, "", nil)
|
||||
gitCmd := NewDummyGitCommandWithRunner(runner)
|
||||
_, err := gitCmd.GetBranchGraph("test")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGitCommandGetAllBranchGraph(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"log", "--graph", "--all", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium"}, args)
|
||||
return secureexec.Command("echo")
|
||||
}
|
||||
cmdStr := gitCmd.Config.GetUserConfig().Git.AllBranchesLogCmd
|
||||
_, err := gitCmd.OSCommand.RunCommandWithOutput(cmdStr)
|
||||
runner := oscommands.NewFakeRunner(t).ExpectArgs([]string{
|
||||
"git", "log", "--graph", "--all", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium",
|
||||
}, "", nil)
|
||||
gitCmd := NewDummyGitCommandWithRunner(runner)
|
||||
cmdStr := gitCmd.UserConfig.Git.AllBranchesLogCmd
|
||||
_, err := gitCmd.Cmd.New(cmdStr).RunWithOutput()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// TestGitCommandCurrentBranchName is a function.
|
||||
func TestGitCommandCurrentBranchName(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
test func(string, string, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"says we are on the master branch if we are",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.Equal(t, "git", cmd)
|
||||
return secureexec.Command("echo", "master")
|
||||
},
|
||||
oscommands.NewFakeRunner(t).Expect(`git symbolic-ref --short HEAD`, "master", nil),
|
||||
func(name string, displayname string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "master", name)
|
||||
@@ -237,20 +179,9 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"falls back to git `git branch --contains` if symbolic-ref fails",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
|
||||
switch args[0] {
|
||||
case "symbolic-ref":
|
||||
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
|
||||
return secureexec.Command("test")
|
||||
case "branch":
|
||||
assert.EqualValues(t, []string{"branch", "--contains"}, args)
|
||||
return secureexec.Command("echo", "* master")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(`git symbolic-ref --short HEAD`, "", errors.New("error")).
|
||||
Expect(`git branch --contains`, "* master", nil),
|
||||
func(name string, displayname string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "master", name)
|
||||
@@ -259,20 +190,9 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"handles a detached head",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
|
||||
switch args[0] {
|
||||
case "symbolic-ref":
|
||||
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
|
||||
return secureexec.Command("test")
|
||||
case "branch":
|
||||
assert.EqualValues(t, []string{"branch", "--contains"}, args)
|
||||
return secureexec.Command("echo", "* (HEAD detached at 123abcd)")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(`git symbolic-ref --short HEAD`, "", errors.New("error")).
|
||||
Expect(`git branch --contains`, "* (HEAD detached at 123abcd)", nil),
|
||||
func(name string, displayname string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "123abcd", name)
|
||||
@@ -281,10 +201,9 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"bubbles up error if there is one",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.Equal(t, "git", cmd)
|
||||
return secureexec.Command("test")
|
||||
},
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(`git symbolic-ref --short HEAD`, "", errors.New("error")).
|
||||
Expect(`git branch --contains`, "", errors.New("error")),
|
||||
func(name string, displayname string, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.EqualValues(t, "", name)
|
||||
@@ -295,19 +214,18 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
gitCmd := NewDummyGitCommandWithRunner(s.runner)
|
||||
s.test(gitCmd.CurrentBranchName())
|
||||
s.runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandResetHard is a function.
|
||||
func TestGitCommandResetHard(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
ref string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
test func(error)
|
||||
}
|
||||
|
||||
@@ -315,23 +233,17 @@ func TestGitCommandResetHard(t *testing.T) {
|
||||
{
|
||||
"valid case",
|
||||
"HEAD",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: `git reset --hard HEAD`,
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(`git reset --hard "HEAD"`, "", nil),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
gitCmd := NewDummyGitCommandWithRunner(s.runner)
|
||||
s.test(gitCmd.ResetHard(s.ref))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,15 +10,20 @@ import (
|
||||
|
||||
// RenameCommit renames the topmost commit with the given name
|
||||
func (c *GitCommand) RenameCommit(name string) error {
|
||||
return c.RunCommand("git commit --allow-empty --amend --only -m %s", c.OSCommand.Quote(name))
|
||||
return c.Cmd.New("git commit --allow-empty --amend --only -m " + c.OSCommand.Quote(name)).Run()
|
||||
}
|
||||
|
||||
// ResetToCommit reset to commit
|
||||
func (c *GitCommand) ResetToCommit(sha string, strength string, options oscommands.RunCommandOptions) error {
|
||||
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git reset --%s %s", strength, sha), options)
|
||||
func (c *GitCommand) ResetToCommit(sha string, strength string, envVars []string) error {
|
||||
return c.Cmd.New(fmt.Sprintf("git reset --%s %s", strength, sha)).
|
||||
// prevents git from prompting us for input which would freeze the program
|
||||
// TODO: see if this is actually needed here
|
||||
AddEnvVars("GIT_TERMINAL_PROMPT=0").
|
||||
AddEnvVars(envVars...).
|
||||
Run()
|
||||
}
|
||||
|
||||
func (c *GitCommand) CommitCmdStr(message string, flags string) string {
|
||||
func (c *GitCommand) CommitCmdObj(message string, flags string) oscommands.ICmdObj {
|
||||
splitMessage := strings.Split(message, "\n")
|
||||
lineArgs := ""
|
||||
for _, line := range splitMessage {
|
||||
@@ -30,43 +35,53 @@ func (c *GitCommand) CommitCmdStr(message string, flags string) string {
|
||||
flagsStr = fmt.Sprintf(" %s", flags)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("git commit%s%s", flagsStr, lineArgs)
|
||||
return c.Cmd.New(fmt.Sprintf("git commit%s%s", flagsStr, lineArgs))
|
||||
}
|
||||
|
||||
// Get the subject of the HEAD commit
|
||||
func (c *GitCommand) GetHeadCommitMessage() (string, error) {
|
||||
cmdStr := "git log -1 --pretty=%s"
|
||||
message, err := c.OSCommand.RunCommandWithOutput(cmdStr)
|
||||
message, err := c.Cmd.New("git log -1 --pretty=%s").RunWithOutput()
|
||||
return strings.TrimSpace(message), err
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetCommitMessage(commitSha string) (string, error) {
|
||||
cmdStr := "git rev-list --format=%B --max-count=1 " + commitSha
|
||||
messageWithHeader, err := c.OSCommand.RunCommandWithOutput(cmdStr)
|
||||
messageWithHeader, err := c.Cmd.New(cmdStr).RunWithOutput()
|
||||
message := strings.Join(strings.SplitAfter(messageWithHeader, "\n")[1:], "\n")
|
||||
return strings.TrimSpace(message), err
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetCommitMessageFirstLine(sha string) (string, error) {
|
||||
return c.Cmd.New(fmt.Sprintf("git show --no-patch --pretty=format:%%s %s", sha)).RunWithOutput()
|
||||
}
|
||||
|
||||
// AmendHead amends HEAD with whatever is staged in your working tree
|
||||
func (c *GitCommand) AmendHead() error {
|
||||
return c.OSCommand.RunCommand(c.AmendHeadCmdStr())
|
||||
return c.AmendHeadCmdObj().Run()
|
||||
}
|
||||
|
||||
func (c *GitCommand) AmendHeadCmdStr() string {
|
||||
return "git commit --amend --no-edit --allow-empty"
|
||||
func (c *GitCommand) AmendHeadCmdObj() oscommands.ICmdObj {
|
||||
return c.Cmd.New("git commit --amend --no-edit --allow-empty")
|
||||
}
|
||||
|
||||
func (c *GitCommand) ShowCmdStr(sha string, filterPath string) string {
|
||||
func (c *GitCommand) ShowCmdObj(sha string, filterPath string) oscommands.ICmdObj {
|
||||
contextSize := c.UserConfig.Git.DiffContextSize
|
||||
filterPathArg := ""
|
||||
if filterPath != "" {
|
||||
filterPathArg = fmt.Sprintf(" -- %s", c.OSCommand.Quote(filterPath))
|
||||
}
|
||||
return fmt.Sprintf("git show --submodule --color=%s --no-renames --stat -p %s %s", c.colorArg(), sha, filterPathArg)
|
||||
|
||||
cmdStr := fmt.Sprintf("git show --submodule --color=%s --unified=%d --no-renames --stat -p %s %s", c.colorArg(), contextSize, sha, filterPathArg)
|
||||
return c.Cmd.New(cmdStr)
|
||||
}
|
||||
|
||||
// Revert reverts the selected commit by sha
|
||||
func (c *GitCommand) Revert(sha string) error {
|
||||
return c.RunCommand("git revert %s", sha)
|
||||
return c.Cmd.New(fmt.Sprintf("git revert %s", sha)).Run()
|
||||
}
|
||||
|
||||
func (c *GitCommand) RevertMerge(sha string, parentNumber int) error {
|
||||
return c.Cmd.New(fmt.Sprintf("git revert %s -m %d", sha, parentNumber)).Run()
|
||||
}
|
||||
|
||||
// CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD
|
||||
@@ -76,15 +91,15 @@ func (c *GitCommand) CherryPickCommits(commits []*models.Commit) error {
|
||||
todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo
|
||||
}
|
||||
|
||||
cmd, err := c.PrepareInteractiveRebaseCommand("HEAD", todo, false)
|
||||
cmdObj, err := c.PrepareInteractiveRebaseCommand("HEAD", todo, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.OSCommand.RunPreparedCommand(cmd)
|
||||
return cmdObj.Run()
|
||||
}
|
||||
|
||||
// CreateFixupCommit creates a commit that fixes up a previous commit
|
||||
func (c *GitCommand) CreateFixupCommit(sha string) error {
|
||||
return c.RunCommand("git commit --fixup=%s", sha)
|
||||
return c.Cmd.New(fmt.Sprintf("git commit --fixup=%s", sha)).Run()
|
||||
}
|
||||
|
||||
@@ -1,43 +1,33 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandRenameCommit is a function.
|
||||
func TestGitCommandRenameCommit(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}
|
||||
runner := oscommands.NewFakeRunner(t).
|
||||
ExpectArgs([]string{"git", "commit", "--allow-empty", "--amend", "--only", "-m", "test"}, "", nil)
|
||||
gitCmd := NewDummyGitCommandWithRunner(runner)
|
||||
|
||||
assert.NoError(t, gitCmd.RenameCommit("test"))
|
||||
runner.CheckForMissingCalls()
|
||||
}
|
||||
|
||||
// TestGitCommandResetToCommit is a function.
|
||||
func TestGitCommandResetToCommit(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"reset", "--hard", "78976bc"}, args)
|
||||
runner := oscommands.NewFakeRunner(t).
|
||||
ExpectArgs([]string{"git", "reset", "--hard", "78976bc"}, "", nil)
|
||||
gitCmd := NewDummyGitCommandWithRunner(runner)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.ResetToCommit("78976bc", "hard", oscommands.RunCommandOptions{}))
|
||||
assert.NoError(t, gitCmd.ResetToCommit("78976bc", "hard", []string{}))
|
||||
runner.CheckForMissingCalls()
|
||||
}
|
||||
|
||||
// TestGitCommandCommitStr is a function.
|
||||
func TestGitCommandCommitStr(t *testing.T) {
|
||||
func TestGitCommandCommitObj(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
type scenario struct {
|
||||
testName string
|
||||
message string
|
||||
@@ -50,62 +40,95 @@ 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)
|
||||
cmdStr := gitCmd.CommitCmdObj(s.message, s.flags).ToString()
|
||||
assert.Equal(t, s.expected, cmdStr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandCreateFixupCommit is a function.
|
||||
func TestGitCommandCreateFixupCommit(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
sha string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
"12345",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: `git commit --fixup=12345`,
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
testName: "valid case",
|
||||
sha: "12345",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git commit --fixup=12345`, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
gitCmd := NewDummyGitCommandWithRunner(s.runner)
|
||||
s.test(gitCmd.CreateFixupCommit(s.sha))
|
||||
s.runner.CheckForMissingCalls()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitCommandShowCmdObj(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
filterPath string
|
||||
contextSize int
|
||||
expected string
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Default case without filter path",
|
||||
filterPath: "",
|
||||
contextSize: 3,
|
||||
expected: "git show --submodule --color=always --unified=3 --no-renames --stat -p 1234567890 ",
|
||||
},
|
||||
{
|
||||
testName: "Default case with filter path",
|
||||
filterPath: "file.txt",
|
||||
contextSize: 3,
|
||||
expected: "git show --submodule --color=always --unified=3 --no-renames --stat -p 1234567890 -- " + gitCmd.OSCommand.Quote("file.txt"),
|
||||
},
|
||||
{
|
||||
testName: "Show diff with custom context size",
|
||||
filterPath: "",
|
||||
contextSize: 77,
|
||||
expected: "git show --submodule --color=always --unified=77 --no-renames --stat -p 1234567890 ",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.UserConfig.Git.DiffContextSize = s.contextSize
|
||||
cmdStr := gitCmd.ShowCmdObj("1234567890", s.filterPath).ToString()
|
||||
assert.Equal(t, s.expected, cmdStr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,16 +15,12 @@ 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 {
|
||||
useConfig := c.Config.GetUserConfig().Git.Paging.UseConfig
|
||||
useConfig := c.UserConfig.Git.Paging.UseConfig
|
||||
if useConfig {
|
||||
pager := c.ConfiguredPager()
|
||||
return strings.Split(pager, "| less")[0]
|
||||
@@ -34,29 +30,21 @@ func (c *GitCommand) GetPager(width int) string {
|
||||
"columnWidth": strconv.Itoa(width/2 - 6),
|
||||
}
|
||||
|
||||
pagerTemplate := c.Config.GetUserConfig().Git.Paging.Pager
|
||||
pagerTemplate := c.UserConfig.Git.Paging.Pager
|
||||
return utils.ResolvePlaceholderString(pagerTemplate, templateValues)
|
||||
}
|
||||
|
||||
func (c *GitCommand) colorArg() string {
|
||||
return c.Config.GetUserConfig().Git.Paging.ColorArg
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetConfigValue(key string) string {
|
||||
output, _ := c.getGitConfigValue(key)
|
||||
return output
|
||||
return c.UserConfig.Git.Paging.ColorArg
|
||||
}
|
||||
|
||||
// UsingGpg tells us whether the user has gpg enabled so that we can know
|
||||
// whether we need to run a subprocess to allow them to enter their password
|
||||
func (c *GitCommand) UsingGpg() bool {
|
||||
overrideGpg := c.Config.GetUserConfig().Git.OverrideGpg
|
||||
overrideGpg := c.UserConfig.Git.OverrideGpg
|
||||
if overrideGpg {
|
||||
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())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
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"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -15,10 +17,18 @@ func NewDummyGitCommand() *GitCommand {
|
||||
// NewDummyGitCommandWithOSCommand creates a new dummy GitCommand for testing
|
||||
func NewDummyGitCommandWithOSCommand(osCommand *oscommands.OSCommand) *GitCommand {
|
||||
return &GitCommand{
|
||||
Log: utils.NewDummyLog(),
|
||||
OSCommand: osCommand,
|
||||
Tr: i18n.NewTranslationSet(utils.NewDummyLog()),
|
||||
Config: config.NewDummyAppConfig(),
|
||||
getGitConfigValue: func(string) (string, error) { return "", nil },
|
||||
Common: utils.NewDummyCommon(),
|
||||
OSCommand: osCommand,
|
||||
GitConfig: git_config.NewFakeGitConfig(map[string]string{}),
|
||||
GetCmdWriter: func() io.Writer { return ioutil.Discard },
|
||||
}
|
||||
}
|
||||
|
||||
func NewDummyGitCommandWithRunner(runner oscommands.ICmdObjRunner) *GitCommand {
|
||||
builder := oscommands.NewDummyCmdObjBuilder(runner)
|
||||
gitCommand := NewDummyGitCommand()
|
||||
gitCommand.Cmd = builder
|
||||
gitCommand.OSCommand.Cmd = builder
|
||||
|
||||
return gitCommand
|
||||
}
|
||||
|
||||
@@ -2,42 +2,50 @@ package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
return "git mergetool"
|
||||
func (c *GitCommand) OpenMergeToolCmdObj() oscommands.ICmdObj {
|
||||
return c.Cmd.New("git mergetool")
|
||||
}
|
||||
|
||||
func (c *GitCommand) OpenMergeTool() error {
|
||||
return c.OSCommand.RunCommand("git mergetool")
|
||||
return c.OpenMergeToolCmdObj().Run()
|
||||
}
|
||||
|
||||
// StageFile stages a file
|
||||
func (c *GitCommand) StageFile(fileName string) error {
|
||||
return c.RunCommand("git add -- %s", c.OSCommand.Quote(fileName))
|
||||
return c.Cmd.New("git add -- " + c.OSCommand.Quote(fileName)).Run()
|
||||
}
|
||||
|
||||
// StageAll stages all files
|
||||
func (c *GitCommand) StageAll() error {
|
||||
return c.RunCommand("git add -A")
|
||||
return c.Cmd.New("git add -A").Run()
|
||||
}
|
||||
|
||||
// UnstageAll unstages all files
|
||||
func (c *GitCommand) UnstageAll() error {
|
||||
return c.RunCommand("git reset")
|
||||
return c.Cmd.New("git reset").Run()
|
||||
}
|
||||
|
||||
// UnStageFile unstages a file
|
||||
@@ -50,7 +58,8 @@ func (c *GitCommand) UnStageFile(fileNames []string, reset bool) error {
|
||||
}
|
||||
|
||||
for _, name := range fileNames {
|
||||
if err := c.OSCommand.RunCommand(command, c.OSCommand.Quote(name)); err != nil {
|
||||
err := c.Cmd.New(fmt.Sprintf(command, c.OSCommand.Quote(name))).Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -67,7 +76,10 @@ func (c *GitCommand) BeforeAndAfterFileForRename(file *models.File) (*models.Fil
|
||||
// all files, passing the --no-renames flag and then recursively call the function
|
||||
// again for the before file and after file.
|
||||
|
||||
filesWithoutRenames := c.GetStatusFiles(GetStatusFileOptions{NoRenames: true})
|
||||
filesWithoutRenames := loaders.
|
||||
NewFileLoader(c.Common, c.Cmd, c.GitConfig).
|
||||
GetStatusFiles(loaders.GetStatusFileOptions{NoRenames: true})
|
||||
|
||||
var beforeFile *models.File
|
||||
var afterFile *models.File
|
||||
for _, f := range filesWithoutRenames {
|
||||
@@ -114,22 +126,22 @@ func (c *GitCommand) DiscardAllFileChanges(file *models.File) error {
|
||||
quotedFileName := c.OSCommand.Quote(file.Name)
|
||||
|
||||
if file.ShortStatus == "AA" {
|
||||
if err := c.RunCommand("git checkout --ours -- %s", quotedFileName); err != nil {
|
||||
if err := c.Cmd.New("git checkout --ours -- " + quotedFileName).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.RunCommand("git add %s", quotedFileName); err != nil {
|
||||
if err := c.Cmd.New("git add -- " + quotedFileName).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if file.ShortStatus == "DU" {
|
||||
return c.RunCommand("git rm %s", quotedFileName)
|
||||
return c.Cmd.New("git rm -- " + quotedFileName).Run()
|
||||
}
|
||||
|
||||
// if the file isn't tracked, we assume you want to delete it
|
||||
if file.HasStagedChanges || file.HasMergeConflicts {
|
||||
if err := c.RunCommand("git reset -- %s", quotedFileName); err != nil {
|
||||
if err := c.Cmd.New("git reset -- " + quotedFileName).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -155,7 +167,7 @@ func (c *GitCommand) DiscardUnstagedDirChanges(node *filetree.FileNode) error {
|
||||
}
|
||||
|
||||
quotedPath := c.OSCommand.Quote(node.GetPath())
|
||||
if err := c.RunCommand("git checkout -- %s", quotedPath); err != nil {
|
||||
if err := c.Cmd.New("git checkout -- " + quotedPath).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -180,7 +192,7 @@ func (c *GitCommand) RemoveUntrackedDirFiles(node *filetree.FileNode) error {
|
||||
// DiscardUnstagedFileChanges directly
|
||||
func (c *GitCommand) DiscardUnstagedFileChanges(file *models.File) error {
|
||||
quotedFileName := c.OSCommand.Quote(file.Name)
|
||||
return c.RunCommand("git checkout -- %s", quotedFileName)
|
||||
return c.Cmd.New("git checkout -- " + quotedFileName).Run()
|
||||
}
|
||||
|
||||
// Ignore adds a file to the gitignore for the repo
|
||||
@@ -189,17 +201,19 @@ 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.WorktreeFileDiffCmdObj(file, plain, cached, ignoreWhitespace).RunWithOutput()
|
||||
return s
|
||||
}
|
||||
|
||||
func (c *GitCommand) WorktreeFileDiffCmdStr(node models.IFile, plain bool, cached bool) string {
|
||||
func (c *GitCommand) WorktreeFileDiffCmdObj(node models.IFile, plain bool, cached bool, ignoreWhitespace bool) oscommands.ICmdObj {
|
||||
cachedArg := ""
|
||||
trackedArg := "--"
|
||||
colorArg := c.colorArg()
|
||||
path := c.OSCommand.Quote(node.GetPath())
|
||||
quotedPath := c.OSCommand.Quote(node.GetPath())
|
||||
ignoreWhitespaceArg := ""
|
||||
contextSize := c.UserConfig.Git.DiffContextSize
|
||||
if cached {
|
||||
cachedArg = "--cached"
|
||||
}
|
||||
@@ -209,12 +223,17 @@ 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)
|
||||
cmdStr := fmt.Sprintf("git diff --submodule --no-ext-diff --unified=%d --color=%s %s %s %s %s", contextSize, colorArg, ignoreWhitespaceArg, cachedArg, trackedArg, quotedPath)
|
||||
|
||||
return c.Cmd.New(cmdStr)
|
||||
}
|
||||
|
||||
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(oscommands.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
|
||||
@@ -225,18 +244,18 @@ func (c *GitCommand) ApplyPatch(patch string, flags ...string) error {
|
||||
flagStr += " --" + flag
|
||||
}
|
||||
|
||||
return c.RunCommand("git apply %s %s", flagStr, c.OSCommand.Quote(filepath))
|
||||
return c.Cmd.New(fmt.Sprintf("git apply%s %s", flagStr, c.OSCommand.Quote(filepath))).Run()
|
||||
}
|
||||
|
||||
// ShowFileDiff get the diff of specified from and to. Typically this will be used for a single commit so it'll be 123abc^..123abc
|
||||
// but when we're in diff mode it could be any 'from' to any 'to'. The reverse flag is also here thanks to diff mode.
|
||||
func (c *GitCommand) ShowFileDiff(from string, to string, reverse bool, fileName string, plain bool) (string, error) {
|
||||
cmdStr := c.ShowFileDiffCmdStr(from, to, reverse, fileName, plain)
|
||||
return c.OSCommand.RunCommandWithOutput(cmdStr)
|
||||
return c.ShowFileDiffCmdObj(from, to, reverse, fileName, plain).RunWithOutput()
|
||||
}
|
||||
|
||||
func (c *GitCommand) ShowFileDiffCmdStr(from string, to string, reverse bool, fileName string, plain bool) string {
|
||||
func (c *GitCommand) ShowFileDiffCmdObj(from string, to string, reverse bool, fileName string, plain bool) oscommands.ICmdObj {
|
||||
colorArg := c.colorArg()
|
||||
contextSize := c.UserConfig.Git.DiffContextSize
|
||||
if plain {
|
||||
colorArg = "never"
|
||||
}
|
||||
@@ -246,12 +265,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 c.Cmd.New(fmt.Sprintf("git diff --submodule --no-ext-diff --unified=%d --no-renames --color=%s %s %s %s -- %s", contextSize, 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.Cmd.New(fmt.Sprintf("git checkout %s -- %s", commitSha, c.OSCommand.Quote(fileName))).Run()
|
||||
}
|
||||
|
||||
// DiscardOldFileChanges discards changes to a file from an old commit
|
||||
@@ -261,7 +280,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.Cmd.New("git cat-file -e HEAD^:" + c.OSCommand.Quote(fileName)).Run(); err != nil {
|
||||
if err := c.OSCommand.Remove(fileName); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -284,28 +303,28 @@ func (c *GitCommand) DiscardOldFileChanges(commits []*models.Commit, commitIndex
|
||||
|
||||
// DiscardAnyUnstagedFileChanges discards any unstages file changes via `git checkout -- .`
|
||||
func (c *GitCommand) DiscardAnyUnstagedFileChanges() error {
|
||||
return c.RunCommand("git checkout -- .")
|
||||
return c.Cmd.New("git checkout -- .").Run()
|
||||
}
|
||||
|
||||
// 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.Cmd.New("git rm -r --cached -- " + c.OSCommand.Quote(name)).Run()
|
||||
}
|
||||
|
||||
// RemoveUntrackedFiles runs `git clean -fd`
|
||||
func (c *GitCommand) RemoveUntrackedFiles() error {
|
||||
return c.RunCommand("git clean -fd")
|
||||
return c.Cmd.New("git clean -fd").Run()
|
||||
}
|
||||
|
||||
// ResetAndClean removes all unstaged changes and removes all untracked files
|
||||
func (c *GitCommand) ResetAndClean() error {
|
||||
submoduleConfigs, err := c.GetSubmoduleConfigs()
|
||||
submoduleConfigs, err := c.Submodules.GetConfigs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(submoduleConfigs) > 0 {
|
||||
if err := c.ResetSubmodules(submoduleConfigs); err != nil {
|
||||
if err := c.Submodules.ResetSubmodules(submoduleConfigs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -317,8 +336,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.UserConfig.OS.EditCommand
|
||||
|
||||
if editor == "" {
|
||||
editor = c.GitConfig.Get("core.editor")
|
||||
}
|
||||
|
||||
if editor == "" {
|
||||
editor = c.OSCommand.Getenv("GIT_EDITOR")
|
||||
@@ -330,13 +353,20 @@ func (c *GitCommand) EditFileCmdStr(filename string) (string, error) {
|
||||
editor = c.OSCommand.Getenv("EDITOR")
|
||||
}
|
||||
if editor == "" {
|
||||
if err := c.OSCommand.RunCommand("which vi"); err == nil {
|
||||
if err := c.OSCommand.Cmd.New("which vi").Run(); err == nil {
|
||||
editor = "vi"
|
||||
}
|
||||
}
|
||||
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.UserConfig.OS.EditCommandTemplate
|
||||
return utils.ResolvePlaceholderString(editCmdTemplate, templateValues), nil
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,22 +1,22 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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/loaders"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// this takes something like:
|
||||
@@ -25,40 +25,57 @@ import (
|
||||
// and returns '264fc6f5' as the second match
|
||||
const CurrentBranchNameRegex = `(?m)^\*.*?([^ ]*?)\)?$`
|
||||
|
||||
type Loaders struct {
|
||||
Commits *loaders.CommitLoader
|
||||
Branches *loaders.BranchLoader
|
||||
Files *loaders.FileLoader
|
||||
CommitFiles *loaders.CommitFileLoader
|
||||
Remotes *loaders.RemoteLoader
|
||||
ReflogCommits *loaders.ReflogCommitLoader
|
||||
Stash *loaders.StashLoader
|
||||
Tags *loaders.TagLoader
|
||||
}
|
||||
|
||||
// GitCommand is our main git interface
|
||||
type GitCommand struct {
|
||||
Log *logrus.Entry
|
||||
*common.Common
|
||||
OSCommand *oscommands.OSCommand
|
||||
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
|
||||
Loaders Loaders
|
||||
|
||||
// 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
|
||||
|
||||
Cmd oscommands.ICmdObjBuilder
|
||||
|
||||
Submodules SubmoduleCommands
|
||||
}
|
||||
|
||||
// NewGitCommand it runs git commands
|
||||
func NewGitCommand(log *logrus.Entry, osCommand *oscommands.OSCommand, tr *i18n.TranslationSet, config config.AppConfigurer) (*GitCommand, error) {
|
||||
func NewGitCommand(
|
||||
cmn *common.Common,
|
||||
osCommand *oscommands.OSCommand,
|
||||
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
|
||||
}
|
||||
|
||||
if repo, err = setupRepository(gogit.PlainOpen, tr.GitconfigParseErr); err != nil {
|
||||
var err error
|
||||
if repo, err = setupRepository(gogit.PlainOpen, cmn.Tr.GitconfigParseErr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -67,18 +84,33 @@ func NewGitCommand(log *logrus.Entry, osCommand *oscommands.OSCommand, tr *i18n.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cmd := NewGitCmdObjBuilder(cmn.Log, osCommand.Cmd)
|
||||
|
||||
gitCommand := &GitCommand{
|
||||
Log: log,
|
||||
OSCommand: osCommand,
|
||||
Tr: tr,
|
||||
Repo: repo,
|
||||
Config: config,
|
||||
getGitConfigValue: getGitConfigValue,
|
||||
DotGitDir: dotGitDir,
|
||||
PushToCurrent: pushToCurrent,
|
||||
Common: cmn,
|
||||
OSCommand: osCommand,
|
||||
Repo: repo,
|
||||
DotGitDir: dotGitDir,
|
||||
PushToCurrent: pushToCurrent,
|
||||
GitConfig: gitConfig,
|
||||
GetCmdWriter: func() io.Writer { return ioutil.Discard },
|
||||
Cmd: cmd,
|
||||
}
|
||||
|
||||
gitCommand.PatchManager = patch.NewPatchManager(log, gitCommand.ApplyPatch, gitCommand.ShowFileDiff)
|
||||
gitCommand.Loaders = Loaders{
|
||||
Commits: loaders.NewCommitLoader(cmn, gitCommand),
|
||||
Branches: loaders.NewBranchLoader(cmn, gitCommand),
|
||||
Files: loaders.NewFileLoader(cmn, cmd, gitConfig),
|
||||
CommitFiles: loaders.NewCommitFileLoader(cmn, cmd),
|
||||
Remotes: loaders.NewRemoteLoader(cmn, cmd, gitCommand.Repo.Remotes),
|
||||
ReflogCommits: loaders.NewReflogCommitLoader(cmn, cmd),
|
||||
Stash: loaders.NewStashLoader(cmn, cmd),
|
||||
Tags: loaders.NewTagLoader(cmn, cmd),
|
||||
}
|
||||
|
||||
gitCommand.Submodules = NewSubmoduleCommands(cmn, cmd, dotGitDir)
|
||||
|
||||
gitCommand.PatchManager = patch.NewPatchManager(gitCommand.Log, gitCommand.ApplyPatch, gitCommand.ShowFileDiff)
|
||||
|
||||
return gitCommand, nil
|
||||
}
|
||||
@@ -95,6 +127,8 @@ func (c *GitCommand) WithSpan(span string) *GitCommand {
|
||||
*newGitCommand = *c
|
||||
newGitCommand.OSCommand = c.OSCommand.WithSpan(span)
|
||||
|
||||
newGitCommand.Cmd = NewGitCmdObjBuilder(c.Log, newGitCommand.OSCommand.Cmd)
|
||||
|
||||
// NOTE: unlike the other things here which create shallow clones, this will
|
||||
// actually update the PatchManager on the original struct to have the new span.
|
||||
// This means each time we call ApplyPatch in PatchManager, we need to ensure
|
||||
@@ -215,34 +249,13 @@ func findDotGitDir(stat func(string) (os.FileInfo, error), readFile func(filenam
|
||||
}
|
||||
|
||||
func VerifyInGitRepo(osCommand *oscommands.OSCommand) error {
|
||||
return osCommand.RunCommand("git rev-parse --git-dir")
|
||||
return osCommand.Cmd.New("git rev-parse --git-dir").Run()
|
||||
}
|
||||
|
||||
func (c *GitCommand) RunCommand(formatString string, formatArgs ...interface{}) error {
|
||||
_, err := c.RunCommandWithOutput(formatString, formatArgs...)
|
||||
return err
|
||||
func (c *GitCommand) GetDotGitDir() string {
|
||||
return c.DotGitDir
|
||||
}
|
||||
|
||||
func (c *GitCommand) RunCommandWithOutput(formatString string, formatArgs ...interface{}) (string, error) {
|
||||
// TODO: have this retry logic in other places we run the command
|
||||
waitTime := 50 * time.Millisecond
|
||||
retryCount := 5
|
||||
attempt := 0
|
||||
|
||||
for {
|
||||
output, err := c.OSCommand.RunCommandWithOutput(formatString, formatArgs...)
|
||||
if err != nil {
|
||||
// if we have an error based on the index lock, we should wait a bit and then retry
|
||||
if strings.Contains(output, ".git/index.lock") {
|
||||
c.Log.Error(output)
|
||||
c.Log.Info("index.lock prevented command from running. Retrying command after a small wait")
|
||||
attempt++
|
||||
time.Sleep(waitTime)
|
||||
if attempt < retryCount {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
return output, err
|
||||
}
|
||||
func (c *GitCommand) GetCmd() oscommands.ICmdObjBuilder {
|
||||
return c.Cmd
|
||||
}
|
||||
|
||||
47
pkg/commands/git_cmd_obj_builder.go
Normal file
47
pkg/commands/git_cmd_obj_builder.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// all we're doing here is wrapping the default command object builder with
|
||||
// some git-specific stuff: e.g. adding a git-specific env var
|
||||
|
||||
type gitCmdObjBuilder struct {
|
||||
innerBuilder *oscommands.CmdObjBuilder
|
||||
}
|
||||
|
||||
var _ oscommands.ICmdObjBuilder = &gitCmdObjBuilder{}
|
||||
|
||||
func NewGitCmdObjBuilder(log *logrus.Entry, innerBuilder *oscommands.CmdObjBuilder) *gitCmdObjBuilder {
|
||||
// the price of having a convenient interface where we can say .New(...).Run() is that our builder now depends on our runner, so when we want to wrap the default builder/runner in new functionality we need to jump through some hoops. We could avoid the use of a decorator function here by just exporting the runner field on the default builder but that would be misleading because we don't want anybody using that to run commands (i.e. we want there to be a single API used across the codebase)
|
||||
updatedBuilder := innerBuilder.CloneWithNewRunner(func(runner oscommands.ICmdObjRunner) oscommands.ICmdObjRunner {
|
||||
return &gitCmdObjRunner{
|
||||
log: log,
|
||||
innerRunner: runner,
|
||||
}
|
||||
})
|
||||
|
||||
return &gitCmdObjBuilder{
|
||||
innerBuilder: updatedBuilder,
|
||||
}
|
||||
}
|
||||
|
||||
var defaultEnvVar = "GIT_OPTIONAL_LOCKS=0"
|
||||
|
||||
func (self *gitCmdObjBuilder) New(cmdStr string) oscommands.ICmdObj {
|
||||
return self.innerBuilder.New(cmdStr).AddEnvVars(defaultEnvVar)
|
||||
}
|
||||
|
||||
func (self *gitCmdObjBuilder) NewFromArgs(args []string) oscommands.ICmdObj {
|
||||
return self.innerBuilder.NewFromArgs(args).AddEnvVars(defaultEnvVar)
|
||||
}
|
||||
|
||||
func (self *gitCmdObjBuilder) NewShell(cmdStr string) oscommands.ICmdObj {
|
||||
return self.innerBuilder.NewShell(cmdStr).AddEnvVars(defaultEnvVar)
|
||||
}
|
||||
|
||||
func (self *gitCmdObjBuilder) Quote(str string) string {
|
||||
return self.innerBuilder.Quote(str)
|
||||
}
|
||||
49
pkg/commands/git_cmd_obj_runner.go
Normal file
49
pkg/commands/git_cmd_obj_runner.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// here we're wrapping the default command runner in some git-specific stuff e.g. retry logic if we get an error due to the presence of .git/index.lock
|
||||
|
||||
type gitCmdObjRunner struct {
|
||||
log *logrus.Entry
|
||||
innerRunner oscommands.ICmdObjRunner
|
||||
}
|
||||
|
||||
func (self *gitCmdObjRunner) Run(cmdObj oscommands.ICmdObj) error {
|
||||
_, err := self.RunWithOutput(cmdObj)
|
||||
return err
|
||||
}
|
||||
|
||||
func (self *gitCmdObjRunner) RunWithOutput(cmdObj oscommands.ICmdObj) (string, error) {
|
||||
// TODO: have this retry logic in other places we run the command
|
||||
waitTime := 50 * time.Millisecond
|
||||
retryCount := 5
|
||||
attempt := 0
|
||||
|
||||
for {
|
||||
output, err := self.innerRunner.RunWithOutput(cmdObj)
|
||||
if err != nil {
|
||||
// if we have an error based on the index lock, we should wait a bit and then retry
|
||||
if strings.Contains(output, ".git/index.lock") {
|
||||
self.log.Error(output)
|
||||
self.log.Info("index.lock prevented command from running. Retrying command after a small wait")
|
||||
attempt++
|
||||
time.Sleep(waitTime)
|
||||
if attempt < retryCount {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
return output, err
|
||||
}
|
||||
}
|
||||
|
||||
func (self *gitCmdObjRunner) RunAndProcessLines(cmdObj oscommands.ICmdObj, onLine func(line string) (bool, error)) error {
|
||||
return self.innerRunner.RunAndProcessLines(cmdObj, onLine)
|
||||
}
|
||||
59
pkg/commands/git_config/cached_git_config.go
Normal file
59
pkg/commands/git_config/cached_git_config.go
Normal file
@@ -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"
|
||||
}
|
||||
116
pkg/commands/git_config/cached_git_config_test.go
Normal file
116
pkg/commands/git_config/cached_git_config_test.go
Normal file
@@ -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)
|
||||
}
|
||||
22
pkg/commands/git_config/fake_git_config.go
Normal file
22
pkg/commands/git_config/fake_git_config.go
Normal file
@@ -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,9 +8,8 @@ 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"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -209,7 +208,7 @@ func TestNewGitCommand(t *testing.T) {
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
s.setup()
|
||||
s.test(NewGitCommand(utils.NewDummyLog(), oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog()), config.NewDummyAppConfig()))
|
||||
s.test(NewGitCommand(utils.NewDummyCommon(), oscommands.NewDummyOSCommand(), git_config.NewFakeGitConfig(nil)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
54
pkg/commands/hosting_service/definitions.go
Normal file
54
pkg/commands/hosting_service/definitions.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package hosting_service
|
||||
|
||||
// if you want to make a custom regex for a given service feel free to test it out
|
||||
// at regoio.herokuapp.com
|
||||
var defaultUrlRegexStrings = []string{
|
||||
`^(?:https?|ssh)://.*/(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
|
||||
`^git@.*:(?P<owner>.*)/(?P<repo>.*?)(?:\.git)?$`,
|
||||
}
|
||||
|
||||
// we've got less type safety using go templates but this lends itself better to
|
||||
// users adding custom service definitions in their config
|
||||
var githubServiceDef = ServiceDefinition{
|
||||
provider: "github",
|
||||
pullRequestURLIntoDefaultBranch: "/compare/{{.From}}?expand=1",
|
||||
pullRequestURLIntoTargetBranch: "/compare/{{.To}}...{{.From}}?expand=1",
|
||||
commitURL: "/commit/{{.CommitSha}}",
|
||||
regexStrings: defaultUrlRegexStrings,
|
||||
}
|
||||
|
||||
var bitbucketServiceDef = ServiceDefinition{
|
||||
provider: "bitbucket",
|
||||
pullRequestURLIntoDefaultBranch: "/pull-requests/new?source={{.From}}&t=1",
|
||||
pullRequestURLIntoTargetBranch: "/pull-requests/new?source={{.From}}&dest={{.To}}&t=1",
|
||||
commitURL: "/commits/{{.CommitSha}}",
|
||||
regexStrings: defaultUrlRegexStrings,
|
||||
}
|
||||
|
||||
var gitLabServiceDef = ServiceDefinition{
|
||||
provider: "gitlab",
|
||||
pullRequestURLIntoDefaultBranch: "/merge_requests/new?merge_request[source_branch]={{.From}}",
|
||||
pullRequestURLIntoTargetBranch: "/merge_requests/new?merge_request[source_branch]={{.From}}&merge_request[target_branch]={{.To}}",
|
||||
commitURL: "/commit/{{.CommitSha}}",
|
||||
regexStrings: defaultUrlRegexStrings,
|
||||
}
|
||||
|
||||
var serviceDefinitions = []ServiceDefinition{githubServiceDef, bitbucketServiceDef, gitLabServiceDef}
|
||||
|
||||
var defaultServiceDomains = []ServiceDomain{
|
||||
{
|
||||
serviceDefinition: githubServiceDef,
|
||||
gitDomain: "github.com",
|
||||
webDomain: "github.com",
|
||||
},
|
||||
{
|
||||
serviceDefinition: bitbucketServiceDef,
|
||||
gitDomain: "bitbucket.org",
|
||||
webDomain: "bitbucket.org",
|
||||
},
|
||||
{
|
||||
serviceDefinition: gitLabServiceDef,
|
||||
gitDomain: "gitlab.com",
|
||||
webDomain: "gitlab.com",
|
||||
},
|
||||
}
|
||||
201
pkg/commands/hosting_service/hosting_service.go
Normal file
201
pkg/commands/hosting_service/hosting_service.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package hosting_service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// This package is for handling logic specific to a git hosting service like github, gitlab, bitbucket, etc.
|
||||
// Different git hosting services have different URL formats for when you want to open a PR or view a commit,
|
||||
// and this package's responsibility is to determine which service you're using based on the remote URL,
|
||||
// and then which URL you need for whatever use case you have.
|
||||
|
||||
type HostingServiceMgr struct {
|
||||
log logrus.FieldLogger
|
||||
tr *i18n.TranslationSet
|
||||
remoteURL string // e.g. https://github.com/jesseduffield/lazygit
|
||||
|
||||
// see https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-pull-request-urls
|
||||
configServiceDomains map[string]string
|
||||
}
|
||||
|
||||
// NewHostingServiceMgr creates new instance of PullRequest
|
||||
func NewHostingServiceMgr(log logrus.FieldLogger, tr *i18n.TranslationSet, remoteURL string, configServiceDomains map[string]string) *HostingServiceMgr {
|
||||
return &HostingServiceMgr{
|
||||
log: log,
|
||||
tr: tr,
|
||||
remoteURL: remoteURL,
|
||||
configServiceDomains: configServiceDomains,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *HostingServiceMgr) GetPullRequestURL(from string, to string) (string, error) {
|
||||
gitService, err := self.getService()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if to == "" {
|
||||
return gitService.getPullRequestURLIntoDefaultBranch(from), nil
|
||||
} else {
|
||||
return gitService.getPullRequestURLIntoTargetBranch(from, to), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (self *HostingServiceMgr) GetCommitURL(commitSha string) (string, error) {
|
||||
gitService, err := self.getService()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pullRequestURL := gitService.getCommitURL(commitSha)
|
||||
|
||||
return pullRequestURL, nil
|
||||
}
|
||||
|
||||
func (self *HostingServiceMgr) getService() (*Service, error) {
|
||||
serviceDomain, err := self.getServiceDomain(self.remoteURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
root, err := serviceDomain.getRootFromRemoteURL(self.remoteURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
root: root,
|
||||
ServiceDefinition: serviceDomain.serviceDefinition,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (self *HostingServiceMgr) getServiceDomain(repoURL string) (*ServiceDomain, error) {
|
||||
candidateServiceDomains := self.getCandidateServiceDomains()
|
||||
|
||||
for _, serviceDomain := range candidateServiceDomains {
|
||||
// I feel like it makes more sense to see if the repo url contains the service domain's git domain,
|
||||
// but I don't want to break anything by changing that right now.
|
||||
if strings.Contains(repoURL, serviceDomain.serviceDefinition.provider) {
|
||||
return &serviceDomain, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New(self.tr.UnsupportedGitService)
|
||||
}
|
||||
|
||||
func (self *HostingServiceMgr) getCandidateServiceDomains() []ServiceDomain {
|
||||
serviceDefinitionByProvider := map[string]ServiceDefinition{}
|
||||
for _, serviceDefinition := range serviceDefinitions {
|
||||
serviceDefinitionByProvider[serviceDefinition.provider] = serviceDefinition
|
||||
}
|
||||
|
||||
var serviceDomains = make([]ServiceDomain, len(defaultServiceDomains))
|
||||
copy(serviceDomains, defaultServiceDomains)
|
||||
|
||||
if len(self.configServiceDomains) > 0 {
|
||||
for gitDomain, typeAndDomain := range self.configServiceDomains {
|
||||
splitData := strings.Split(typeAndDomain, ":")
|
||||
if len(splitData) != 2 {
|
||||
self.log.Errorf("Unexpected format for git service: '%s'. Expected something like 'github.com:github.com'", typeAndDomain)
|
||||
continue
|
||||
}
|
||||
|
||||
provider := splitData[0]
|
||||
webDomain := splitData[1]
|
||||
|
||||
serviceDefinition, ok := serviceDefinitionByProvider[provider]
|
||||
if !ok {
|
||||
providerNames := []string{}
|
||||
for _, serviceDefinition := range serviceDefinitions {
|
||||
providerNames = append(providerNames, serviceDefinition.provider)
|
||||
}
|
||||
self.log.Errorf("Unknown git service type: '%s'. Expected one of %s", provider, strings.Join(providerNames, ", "))
|
||||
continue
|
||||
}
|
||||
|
||||
serviceDomains = append(serviceDomains, ServiceDomain{
|
||||
gitDomain: gitDomain,
|
||||
webDomain: webDomain,
|
||||
serviceDefinition: serviceDefinition,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return serviceDomains
|
||||
}
|
||||
|
||||
// a service domains pairs a service definition with the actual domain it's being served from.
|
||||
// Sometimes the git service is hosted in a custom domains so although it'll use say
|
||||
// the github service definition, it'll actually be served from e.g. my-custom-github.com
|
||||
type ServiceDomain struct {
|
||||
gitDomain string // the one that appears in the git remote url
|
||||
webDomain string // the one that appears in the web url
|
||||
serviceDefinition ServiceDefinition
|
||||
}
|
||||
|
||||
func (self ServiceDomain) getRootFromRemoteURL(repoURL string) (string, error) {
|
||||
// we may want to make this more specific to the service in future e.g. if
|
||||
// some new service comes along which has a different root url structure.
|
||||
repoInfo, err := self.serviceDefinition.getRepoInfoFromURL(repoURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("https://%s/%s/%s", self.webDomain, repoInfo.Owner, repoInfo.Repository), nil
|
||||
}
|
||||
|
||||
// RepoInformation holds some basic information about the repo
|
||||
type RepoInformation struct {
|
||||
Owner string
|
||||
Repository string
|
||||
}
|
||||
|
||||
type ServiceDefinition struct {
|
||||
provider string
|
||||
pullRequestURLIntoDefaultBranch string
|
||||
pullRequestURLIntoTargetBranch string
|
||||
commitURL string
|
||||
regexStrings []string
|
||||
}
|
||||
|
||||
func (self ServiceDefinition) getRepoInfoFromURL(url string) (*RepoInformation, error) {
|
||||
for _, regexStr := range self.regexStrings {
|
||||
re := regexp.MustCompile(regexStr)
|
||||
matches := utils.FindNamedMatches(re, url)
|
||||
if matches != nil {
|
||||
return &RepoInformation{
|
||||
Owner: matches["owner"],
|
||||
Repository: matches["repo"],
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("Failed to parse repo information from url")
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
root string
|
||||
ServiceDefinition
|
||||
}
|
||||
|
||||
func (self *Service) getPullRequestURLIntoDefaultBranch(from string) string {
|
||||
return self.resolveUrl(self.pullRequestURLIntoDefaultBranch, map[string]string{"From": from})
|
||||
}
|
||||
|
||||
func (self *Service) getPullRequestURLIntoTargetBranch(from string, to string) string {
|
||||
return self.resolveUrl(self.pullRequestURLIntoTargetBranch, map[string]string{"From": from, "To": to})
|
||||
}
|
||||
|
||||
func (self *Service) getCommitURL(commitSha string) string {
|
||||
return self.resolveUrl(self.commitURL, map[string]string{"CommitSha": commitSha})
|
||||
}
|
||||
|
||||
func (self *Service) resolveUrl(templateString string, args map[string]string) string {
|
||||
return self.root + utils.ResolvePlaceholderString(templateString, args)
|
||||
}
|
||||
233
pkg/commands/hosting_service/hosting_service_test.go
Normal file
233
pkg/commands/hosting_service/hosting_service_test.go
Normal file
@@ -0,0 +1,233 @@
|
||||
package hosting_service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetRepoInfoFromURL(t *testing.T) {
|
||||
type scenario struct {
|
||||
serviceDefinition ServiceDefinition
|
||||
testName string
|
||||
repoURL string
|
||||
test func(*RepoInformation)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
githubServiceDef,
|
||||
"Returns repository information for git remote url",
|
||||
"git@github.com:petersmith/super_calculator",
|
||||
func(repoInfo *RepoInformation) {
|
||||
assert.EqualValues(t, repoInfo.Owner, "petersmith")
|
||||
assert.EqualValues(t, repoInfo.Repository, "super_calculator")
|
||||
},
|
||||
},
|
||||
{
|
||||
githubServiceDef,
|
||||
"Returns repository information for git remote url, trimming trailing '.git'",
|
||||
"git@github.com:petersmith/super_calculator.git",
|
||||
func(repoInfo *RepoInformation) {
|
||||
assert.EqualValues(t, repoInfo.Owner, "petersmith")
|
||||
assert.EqualValues(t, repoInfo.Repository, "super_calculator")
|
||||
},
|
||||
},
|
||||
{
|
||||
githubServiceDef,
|
||||
"Returns repository information for ssh remote url",
|
||||
"ssh://git@github.com/petersmith/super_calculator",
|
||||
func(repoInfo *RepoInformation) {
|
||||
assert.EqualValues(t, repoInfo.Owner, "petersmith")
|
||||
assert.EqualValues(t, repoInfo.Repository, "super_calculator")
|
||||
},
|
||||
},
|
||||
{
|
||||
githubServiceDef,
|
||||
"Returns repository information for http remote url",
|
||||
"https://my_username@bitbucket.org/johndoe/social_network.git",
|
||||
func(repoInfo *RepoInformation) {
|
||||
assert.EqualValues(t, repoInfo.Owner, "johndoe")
|
||||
assert.EqualValues(t, repoInfo.Repository, "social_network")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
result, err := s.serviceDefinition.getRepoInfoFromURL(s.repoURL)
|
||||
assert.NoError(t, err)
|
||||
s.test(result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPullRequestURL(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
from string
|
||||
to string
|
||||
remoteUrl string
|
||||
configServiceDomains map[string]string
|
||||
test func(url string, err error)
|
||||
expectedLoggedErrors []string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket",
|
||||
from: "feature/profile-page",
|
||||
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
test: func(url string, err error) {
|
||||
assert.EqualError(t, err, "Unsupported git service")
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Does not log error when config service domains are valid",
|
||||
from: "feature/profile-page",
|
||||
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
|
||||
configServiceDomains: map[string]string{
|
||||
// valid configuration for a custom service URL
|
||||
"git.work.com": "gitlab:code.work.com",
|
||||
},
|
||||
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)
|
||||
},
|
||||
expectedLoggedErrors: nil,
|
||||
},
|
||||
{
|
||||
testName: "Logs error when config service domain is malformed",
|
||||
from: "feature/profile-page",
|
||||
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
|
||||
configServiceDomains: map[string]string{
|
||||
"noservice.work.com": "noservice.work.com",
|
||||
},
|
||||
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)
|
||||
},
|
||||
expectedLoggedErrors: []string{"Unexpected format for git service: 'noservice.work.com'. Expected something like 'github.com:github.com'"},
|
||||
},
|
||||
{
|
||||
testName: "Logs error when config service domain uses unknown provider",
|
||||
from: "feature/profile-page",
|
||||
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
|
||||
configServiceDomains: map[string]string{
|
||||
"invalid.work.com": "noservice:invalid.work.com",
|
||||
},
|
||||
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)
|
||||
},
|
||||
expectedLoggedErrors: []string{"Unknown git service type: 'noservice'. Expected one of github, bitbucket, gitlab"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
tr := i18n.EnglishTranslationSet()
|
||||
log := &test.FakeFieldLogger{}
|
||||
hostingServiceMgr := NewHostingServiceMgr(log, &tr, s.remoteUrl, s.configServiceDomains)
|
||||
s.test(hostingServiceMgr.GetPullRequestURL(s.from, s.to))
|
||||
log.AssertErrors(t, s.expectedLoggedErrors)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
package commands
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// context:
|
||||
@@ -20,25 +20,76 @@ import (
|
||||
// if we find out we need to use one of these functions in the git.go file, we
|
||||
// can just pull them out of here and put them there and then call them from in here
|
||||
|
||||
// BranchListBuilder returns a list of Branch objects for the current repo
|
||||
type BranchListBuilder struct {
|
||||
Log *logrus.Entry
|
||||
GitCommand *GitCommand
|
||||
ReflogCommits []*models.Commit
|
||||
// BranchLoader returns a list of Branch objects for the current repo
|
||||
type BranchLoader struct {
|
||||
*common.Common
|
||||
getRawBranches func() (string, error)
|
||||
getCurrentBranchName func() (string, string, error)
|
||||
}
|
||||
|
||||
// NewBranchListBuilder builds a new branch list builder
|
||||
func NewBranchListBuilder(log *logrus.Entry, gitCommand *GitCommand, reflogCommits []*models.Commit) (*BranchListBuilder, error) {
|
||||
return &BranchListBuilder{
|
||||
Log: log,
|
||||
GitCommand: gitCommand,
|
||||
ReflogCommits: reflogCommits,
|
||||
}, nil
|
||||
type BranchLoaderGitCommand interface {
|
||||
GetRawBranches() (string, error)
|
||||
CurrentBranchName() (string, string, error)
|
||||
}
|
||||
|
||||
func (b *BranchListBuilder) obtainBranches() []*models.Branch {
|
||||
cmdStr := `git for-each-ref --sort=-committerdate --format="%(HEAD)|%(refname:short)|%(upstream:short)|%(upstream:track)" refs/heads`
|
||||
output, err := b.GitCommand.OSCommand.RunCommandWithOutput(cmdStr)
|
||||
func NewBranchLoader(
|
||||
cmn *common.Common,
|
||||
gitCommand BranchLoaderGitCommand,
|
||||
) *BranchLoader {
|
||||
return &BranchLoader{
|
||||
Common: cmn,
|
||||
getRawBranches: gitCommand.GetRawBranches,
|
||||
getCurrentBranchName: gitCommand.CurrentBranchName,
|
||||
}
|
||||
}
|
||||
|
||||
// Load the list of branches for the current repo
|
||||
func (self *BranchLoader) Load(reflogCommits []*models.Commit) []*models.Branch {
|
||||
branches := self.obtainBranches()
|
||||
|
||||
reflogBranches := self.obtainReflogBranches(reflogCommits)
|
||||
|
||||
// loop through reflog branches. If there is a match, merge them, then remove it from the branches and keep it in the reflog branches
|
||||
branchesWithRecency := make([]*models.Branch, 0)
|
||||
outer:
|
||||
for _, reflogBranch := range reflogBranches {
|
||||
for j, branch := range branches {
|
||||
if branch.Head {
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(reflogBranch.Name, branch.Name) {
|
||||
branch.Recency = reflogBranch.Recency
|
||||
branchesWithRecency = append(branchesWithRecency, branch)
|
||||
branches = append(branches[0:j], branches[j+1:]...)
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
branches = append(branchesWithRecency, branches...)
|
||||
|
||||
foundHead := false
|
||||
for i, branch := range branches {
|
||||
if branch.Head {
|
||||
foundHead = true
|
||||
branch.Recency = " *"
|
||||
branches = append(branches[0:i], branches[i+1:]...)
|
||||
branches = append([]*models.Branch{branch}, branches...)
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundHead {
|
||||
currentBranchName, currentBranchDisplayName, err := self.getCurrentBranchName()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
branches = append([]*models.Branch{{Name: currentBranchName, DisplayName: currentBranchDisplayName, Head: true, Recency: " *"}}, branches...)
|
||||
}
|
||||
return branches
|
||||
}
|
||||
|
||||
func (self *BranchLoader) obtainBranches() []*models.Branch {
|
||||
output, err := self.getRawBranches()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -52,6 +103,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{
|
||||
@@ -92,58 +149,13 @@ func (b *BranchListBuilder) obtainBranches() []*models.Branch {
|
||||
return branches
|
||||
}
|
||||
|
||||
// Build the list of branches for the current repo
|
||||
func (b *BranchListBuilder) Build() []*models.Branch {
|
||||
branches := b.obtainBranches()
|
||||
|
||||
reflogBranches := b.obtainReflogBranches()
|
||||
|
||||
// loop through reflog branches. If there is a match, merge them, then remove it from the branches and keep it in the reflog branches
|
||||
branchesWithRecency := make([]*models.Branch, 0)
|
||||
outer:
|
||||
for _, reflogBranch := range reflogBranches {
|
||||
for j, branch := range branches {
|
||||
if branch.Head {
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(reflogBranch.Name, branch.Name) {
|
||||
branch.Recency = reflogBranch.Recency
|
||||
branchesWithRecency = append(branchesWithRecency, branch)
|
||||
branches = append(branches[0:j], branches[j+1:]...)
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
branches = append(branchesWithRecency, branches...)
|
||||
|
||||
foundHead := false
|
||||
for i, branch := range branches {
|
||||
if branch.Head {
|
||||
foundHead = true
|
||||
branch.Recency = " *"
|
||||
branches = append(branches[0:i], branches[i+1:]...)
|
||||
branches = append([]*models.Branch{branch}, branches...)
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundHead {
|
||||
currentBranchName, currentBranchDisplayName, err := b.GitCommand.CurrentBranchName()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
branches = append([]*models.Branch{{Name: currentBranchName, DisplayName: currentBranchDisplayName, Head: true, Recency: " *"}}, branches...)
|
||||
}
|
||||
return branches
|
||||
}
|
||||
|
||||
// TODO: only look at the new reflog commits, and otherwise store the recencies in
|
||||
// int form against the branch to recalculate the time ago
|
||||
func (b *BranchListBuilder) obtainReflogBranches() []*models.Branch {
|
||||
func (self *BranchLoader) obtainReflogBranches(reflogCommits []*models.Commit) []*models.Branch {
|
||||
foundBranchesMap := map[string]bool{}
|
||||
re := regexp.MustCompile(`checkout: moving from ([\S]+) to ([\S]+)`)
|
||||
reflogBranches := make([]*models.Branch, 0, len(b.ReflogCommits))
|
||||
for _, commit := range b.ReflogCommits {
|
||||
reflogBranches := make([]*models.Branch, 0, len(reflogCommits))
|
||||
for _, commit := range reflogCommits {
|
||||
if match := re.FindStringSubmatch(commit.Name); len(match) == 3 {
|
||||
recency := utils.UnixToTimeAgo(commit.UnixTimestamp)
|
||||
for _, branchName := range match[1:] {
|
||||
57
pkg/commands/loaders/commit_files.go
Normal file
57
pkg/commands/loaders/commit_files.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
)
|
||||
|
||||
type CommitFileLoader struct {
|
||||
*common.Common
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
}
|
||||
|
||||
func NewCommitFileLoader(common *common.Common, cmd oscommands.ICmdObjBuilder) *CommitFileLoader {
|
||||
return &CommitFileLoader{
|
||||
Common: common,
|
||||
cmd: cmd,
|
||||
}
|
||||
}
|
||||
|
||||
// GetFilesInDiff get the specified commit files
|
||||
func (self *CommitFileLoader) GetFilesInDiff(from string, to string, reverse bool) ([]*models.CommitFile, error) {
|
||||
reverseFlag := ""
|
||||
if reverse {
|
||||
reverseFlag = " -R "
|
||||
}
|
||||
|
||||
filenames, err := self.cmd.New(fmt.Sprintf("git diff --submodule --no-ext-diff --name-status -z --no-renames %s %s %s", reverseFlag, from, to)).RunWithOutput()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return self.getCommitFilesFromFilenames(filenames), nil
|
||||
}
|
||||
|
||||
// filenames string is something like "file1\nfile2\nfile3"
|
||||
func (self *CommitFileLoader) getCommitFilesFromFilenames(filenames string) []*models.CommitFile {
|
||||
commitFiles := make([]*models.CommitFile, 0)
|
||||
|
||||
lines := strings.Split(strings.TrimRight(filenames, "\x00"), "\x00")
|
||||
n := len(lines)
|
||||
for i := 0; i < n-1; i += 2 {
|
||||
// typical result looks like 'A my_file' meaning my_file was added
|
||||
changeStatus := lines[i]
|
||||
name := lines[i+1]
|
||||
|
||||
commitFiles = append(commitFiles, &models.CommitFile{
|
||||
Name: name,
|
||||
ChangeStatus: changeStatus,
|
||||
})
|
||||
}
|
||||
|
||||
return commitFiles
|
||||
}
|
||||
@@ -1,20 +1,19 @@
|
||||
package commands
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
)
|
||||
|
||||
// context:
|
||||
@@ -23,34 +22,146 @@ import (
|
||||
// be processed as part of a rebase (these won't appear in git log but we
|
||||
// grab them from the rebase-related files in the .git directory to show them
|
||||
|
||||
// if we find out we need to use one of these functions in the git.go file, we
|
||||
// can just pull them out of here and put them there and then call them from in here
|
||||
|
||||
const SEPARATION_CHAR = "|"
|
||||
|
||||
// CommitListBuilder returns a list of Branch objects for the current repo
|
||||
type CommitListBuilder struct {
|
||||
Log *logrus.Entry
|
||||
GitCommand *GitCommand
|
||||
OSCommand *oscommands.OSCommand
|
||||
Tr *i18n.TranslationSet
|
||||
// CommitLoader returns a list of Commit objects for the current repo
|
||||
type CommitLoader struct {
|
||||
*common.Common
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
|
||||
getCurrentBranchName func() (string, string, error)
|
||||
getRebaseMode func() (enums.RebaseMode, error)
|
||||
readFile func(filename string) ([]byte, error)
|
||||
walkFiles func(root string, fn filepath.WalkFunc) error
|
||||
dotGitDir string
|
||||
}
|
||||
|
||||
// NewCommitListBuilder builds a new commit list builder
|
||||
func NewCommitListBuilder(log *logrus.Entry, gitCommand *GitCommand, osCommand *oscommands.OSCommand, tr *i18n.TranslationSet) *CommitListBuilder {
|
||||
return &CommitListBuilder{
|
||||
Log: log,
|
||||
GitCommand: gitCommand,
|
||||
OSCommand: osCommand,
|
||||
Tr: tr,
|
||||
type CommitLoaderGitCommand interface {
|
||||
CurrentBranchName() (string, string, error)
|
||||
RebaseMode() (enums.RebaseMode, error)
|
||||
GetCmd() oscommands.ICmdObjBuilder
|
||||
GetDotGitDir() string
|
||||
}
|
||||
|
||||
// making our dependencies explicit for the sake of easier testing
|
||||
func NewCommitLoader(
|
||||
cmn *common.Common,
|
||||
gitCommand CommitLoaderGitCommand,
|
||||
) *CommitLoader {
|
||||
return &CommitLoader{
|
||||
Common: cmn,
|
||||
cmd: gitCommand.GetCmd(),
|
||||
getCurrentBranchName: gitCommand.CurrentBranchName,
|
||||
getRebaseMode: gitCommand.RebaseMode,
|
||||
readFile: ioutil.ReadFile,
|
||||
walkFiles: filepath.Walk,
|
||||
dotGitDir: gitCommand.GetDotGitDir(),
|
||||
}
|
||||
}
|
||||
|
||||
type GetCommitsOptions struct {
|
||||
Limit bool
|
||||
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
|
||||
}
|
||||
|
||||
// GetCommits obtains the commits of the current branch
|
||||
func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit, error) {
|
||||
commits := []*models.Commit{}
|
||||
var rebasingCommits []*models.Commit
|
||||
rebaseMode, err := self.getRebaseMode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if opts.IncludeRebaseCommits && opts.FilterPath == "" {
|
||||
var err error
|
||||
rebasingCommits, err = self.MergeRebasingCommits(commits)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
commits = append(commits, rebasingCommits...)
|
||||
}
|
||||
|
||||
passedFirstPushedCommit := false
|
||||
firstPushedCommit, err := self.getFirstPushedCommit(opts.RefName)
|
||||
if err != nil {
|
||||
// must have no upstream branch so we'll consider everything as pushed
|
||||
passedFirstPushedCommit = true
|
||||
}
|
||||
|
||||
err = self.getLogCmd(opts).RunAndProcessLines(func(line string) (bool, error) {
|
||||
if canExtractCommit(line) {
|
||||
commit := self.extractCommitFromLine(line)
|
||||
if commit.Sha == firstPushedCommit {
|
||||
passedFirstPushedCommit = true
|
||||
}
|
||||
commit.Status = map[bool]string{true: "unpushed", false: "pushed"}[!passedFirstPushedCommit]
|
||||
commits = append(commits, commit)
|
||||
}
|
||||
return false, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(commits) == 0 {
|
||||
return commits, nil
|
||||
}
|
||||
|
||||
if rebaseMode != enums.REBASE_MODE_NONE {
|
||||
currentCommit := commits[len(rebasingCommits)]
|
||||
youAreHere := style.FgYellow.Sprintf("<-- %s ---", self.Tr.YouAreHere)
|
||||
currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name)
|
||||
}
|
||||
|
||||
commits, err = self.setCommitMergedStatuses(opts.RefName, commits)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return commits, nil
|
||||
}
|
||||
|
||||
func (self *CommitLoader) MergeRebasingCommits(commits []*models.Commit) ([]*models.Commit, error) {
|
||||
// chances are we have as many commits as last time so we'll set the capacity to be the old length
|
||||
result := make([]*models.Commit, 0, len(commits))
|
||||
for i, commit := range commits {
|
||||
if commit.Status != "rebasing" { // removing the existing rebase commits so we can add the refreshed ones
|
||||
result = append(result, commits[i:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
rebaseMode, err := self.getRebaseMode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if rebaseMode == enums.REBASE_MODE_NONE {
|
||||
// not in rebase mode so return original commits
|
||||
return result, nil
|
||||
}
|
||||
|
||||
rebasingCommits, err := self.getHydratedRebasingCommits(rebaseMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rebasingCommits) > 0 {
|
||||
result = append(rebasingCommits, result...)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// extractCommitFromLine takes a line from a git log and extracts the sha, message, date, and tag if present
|
||||
// then puts them into a commit object
|
||||
// example input:
|
||||
// 8ad01fe32fcc20f07bc6693f87aa4977c327f1e1|10 hours ago|Jesse Duffield| (HEAD -> master, tag: v0.15.2)|refresh commits when adding a tag
|
||||
func (c *CommitListBuilder) extractCommitFromLine(line string) *models.Commit {
|
||||
func (self *CommitLoader) extractCommitFromLine(line string) *models.Commit {
|
||||
split := strings.Split(line, SEPARATION_CHAR)
|
||||
|
||||
sha := split[0]
|
||||
@@ -72,10 +183,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,120 +190,70 @@ func (c *CommitListBuilder) extractCommitFromLine(line string) *models.Commit {
|
||||
ExtraInfo: extraInfo,
|
||||
UnixTimestamp: int64(unitTimestampInt),
|
||||
Author: author,
|
||||
IsMerge: isMerge,
|
||||
Parents: strings.Split(parentHashes, " "),
|
||||
}
|
||||
}
|
||||
|
||||
type GetCommitsOptions struct {
|
||||
Limit bool
|
||||
FilterPath string
|
||||
IncludeRebaseCommits bool
|
||||
RefName string // e.g. "HEAD" or "my_branch"
|
||||
}
|
||||
func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode) ([]*models.Commit, error) {
|
||||
commits, err := self.getRebasingCommits(rebaseMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (c *CommitListBuilder) MergeRebasingCommits(commits []*models.Commit) ([]*models.Commit, error) {
|
||||
// chances are we have as many commits as last time so we'll set the capacity to be the old length
|
||||
result := make([]*models.Commit, 0, len(commits))
|
||||
if len(commits) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
commitShas := make([]string, len(commits))
|
||||
for i, commit := range commits {
|
||||
if commit.Status != "rebasing" { // removing the existing rebase commits so we can add the refreshed ones
|
||||
result = append(result, commits[i:]...)
|
||||
break
|
||||
}
|
||||
commitShas[i] = commit.Sha
|
||||
}
|
||||
|
||||
rebaseMode, err := c.GitCommand.RebaseMode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// note that we're not filtering these as we do non-rebasing commits just because
|
||||
// I suspect that will cause some damage
|
||||
cmdObj := self.cmd.New(
|
||||
fmt.Sprintf(
|
||||
"git show %s --no-patch --oneline %s --abbrev=%d",
|
||||
strings.Join(commitShas, " "),
|
||||
prettyFormat,
|
||||
20,
|
||||
),
|
||||
)
|
||||
|
||||
if rebaseMode == "" {
|
||||
// not in rebase mode so return original commits
|
||||
return result, nil
|
||||
}
|
||||
|
||||
rebasingCommits, err := c.getRebasingCommits(rebaseMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rebasingCommits) > 0 {
|
||||
result = append(rebasingCommits, result...)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetCommits obtains the commits of the current branch
|
||||
func (c *CommitListBuilder) GetCommits(opts GetCommitsOptions) ([]*models.Commit, error) {
|
||||
commits := []*models.Commit{}
|
||||
var rebasingCommits []*models.Commit
|
||||
rebaseMode, err := c.GitCommand.RebaseMode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if opts.IncludeRebaseCommits && opts.FilterPath == "" {
|
||||
var err error
|
||||
rebasingCommits, err = c.MergeRebasingCommits(commits)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
commits = append(commits, rebasingCommits...)
|
||||
}
|
||||
|
||||
passedFirstPushedCommit := false
|
||||
firstPushedCommit, err := c.getFirstPushedCommit(opts.RefName)
|
||||
if err != nil {
|
||||
// must have no upstream branch so we'll consider everything as pushed
|
||||
passedFirstPushedCommit = true
|
||||
}
|
||||
|
||||
cmd := c.getLogCmd(opts)
|
||||
|
||||
err = oscommands.RunLineOutputCmd(cmd, func(line string) (bool, error) {
|
||||
if strings.Split(line, " ")[0] != "gpg:" {
|
||||
commit := c.extractCommitFromLine(line)
|
||||
if commit.Sha == firstPushedCommit {
|
||||
passedFirstPushedCommit = true
|
||||
}
|
||||
commit.Status = map[bool]string{true: "unpushed", false: "pushed"}[!passedFirstPushedCommit]
|
||||
commits = append(commits, commit)
|
||||
hydratedCommits := make([]*models.Commit, 0, len(commits))
|
||||
i := 0
|
||||
err = cmdObj.RunAndProcessLines(func(line string) (bool, error) {
|
||||
if canExtractCommit(line) {
|
||||
commit := self.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
|
||||
}
|
||||
|
||||
if rebaseMode != "" {
|
||||
currentCommit := commits[len(rebasingCommits)]
|
||||
blue := color.New(color.FgYellow)
|
||||
youAreHere := blue.Sprintf("<-- %s ---", c.Tr.YouAreHere)
|
||||
currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name)
|
||||
}
|
||||
|
||||
commits, err = c.setCommitMergedStatuses(opts.RefName, commits)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return commits, nil
|
||||
return hydratedCommits, nil
|
||||
}
|
||||
|
||||
// getRebasingCommits obtains the commits that we're in the process of rebasing
|
||||
func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*models.Commit, error) {
|
||||
func (self *CommitLoader) getRebasingCommits(rebaseMode enums.RebaseMode) ([]*models.Commit, error) {
|
||||
switch rebaseMode {
|
||||
case REBASE_MODE_MERGING:
|
||||
return c.getNormalRebasingCommits()
|
||||
case REBASE_MODE_INTERACTIVE:
|
||||
return c.getInteractiveRebasingCommits()
|
||||
case enums.REBASE_MODE_MERGING:
|
||||
return self.getNormalRebasingCommits()
|
||||
case enums.REBASE_MODE_INTERACTIVE:
|
||||
return self.getInteractiveRebasingCommits()
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CommitListBuilder) getNormalRebasingCommits() ([]*models.Commit, error) {
|
||||
func (self *CommitLoader) getNormalRebasingCommits() ([]*models.Commit, error) {
|
||||
rewrittenCount := 0
|
||||
bytesContent, err := ioutil.ReadFile(filepath.Join(c.GitCommand.DotGitDir, "rebase-apply/rewritten"))
|
||||
bytesContent, err := self.readFile(filepath.Join(self.dotGitDir, "rebase-apply/rewritten"))
|
||||
if err == nil {
|
||||
content := string(bytesContent)
|
||||
rewrittenCount = len(strings.Split(content, "\n"))
|
||||
@@ -204,7 +261,7 @@ func (c *CommitListBuilder) getNormalRebasingCommits() ([]*models.Commit, error)
|
||||
|
||||
// we know we're rebasing, so lets get all the files whose names have numbers
|
||||
commits := []*models.Commit{}
|
||||
err = filepath.Walk(filepath.Join(c.GitCommand.DotGitDir, "rebase-apply"), func(path string, f os.FileInfo, err error) error {
|
||||
err = self.walkFiles(filepath.Join(self.dotGitDir, "rebase-apply"), func(path string, f os.FileInfo, err error) error {
|
||||
if rewrittenCount > 0 {
|
||||
rewrittenCount--
|
||||
return nil
|
||||
@@ -216,12 +273,12 @@ func (c *CommitListBuilder) getNormalRebasingCommits() ([]*models.Commit, error)
|
||||
if !re.MatchString(f.Name()) {
|
||||
return nil
|
||||
}
|
||||
bytesContent, err := ioutil.ReadFile(path)
|
||||
bytesContent, err := self.readFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content := string(bytesContent)
|
||||
commit, err := c.commitFromPatch(content)
|
||||
commit, err := self.commitFromPatch(content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -247,10 +304,10 @@ func (c *CommitListBuilder) getNormalRebasingCommits() ([]*models.Commit, error)
|
||||
// getInteractiveRebasingCommits takes our git-rebase-todo and our git-rebase-todo.backup files
|
||||
// and extracts out the sha and names of commits that we still have to go
|
||||
// in the rebase:
|
||||
func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*models.Commit, error) {
|
||||
bytesContent, err := ioutil.ReadFile(filepath.Join(c.GitCommand.DotGitDir, "rebase-merge/git-rebase-todo"))
|
||||
func (self *CommitLoader) getInteractiveRebasingCommits() ([]*models.Commit, error) {
|
||||
bytesContent, err := self.readFile(filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo"))
|
||||
if err != nil {
|
||||
c.Log.Error(fmt.Sprintf("error occurred reading git-rebase-todo: %s", err.Error()))
|
||||
self.Log.Error(fmt.Sprintf("error occurred reading git-rebase-todo: %s", err.Error()))
|
||||
// we assume an error means the file doesn't exist so we just return
|
||||
return nil, nil
|
||||
}
|
||||
@@ -281,7 +338,7 @@ func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*models.Commit, e
|
||||
// From: Lazygit Tester <test@example.com>
|
||||
// Date: Wed, 5 Dec 2018 21:03:23 +1100
|
||||
// Subject: second commit on master
|
||||
func (c *CommitListBuilder) commitFromPatch(content string) (*models.Commit, error) {
|
||||
func (self *CommitLoader) commitFromPatch(content string) (*models.Commit, error) {
|
||||
lines := strings.Split(content, "\n")
|
||||
sha := strings.Split(lines[0], " ")[1]
|
||||
name := strings.TrimPrefix(lines[3], "Subject: ")
|
||||
@@ -292,8 +349,8 @@ func (c *CommitListBuilder) commitFromPatch(content string) (*models.Commit, err
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *CommitListBuilder) setCommitMergedStatuses(refName string, commits []*models.Commit) ([]*models.Commit, error) {
|
||||
ancestor, err := c.getMergeBase(refName)
|
||||
func (self *CommitLoader) setCommitMergedStatuses(refName string, commits []*models.Commit) ([]*models.Commit, error) {
|
||||
ancestor, err := self.getMergeBase(refName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -315,8 +372,8 @@ func (c *CommitListBuilder) setCommitMergedStatuses(refName string, commits []*m
|
||||
return commits, nil
|
||||
}
|
||||
|
||||
func (c *CommitListBuilder) getMergeBase(refName string) (string, error) {
|
||||
currentBranch, _, err := c.GitCommand.CurrentBranchName()
|
||||
func (self *CommitLoader) getMergeBase(refName string) (string, error) {
|
||||
currentBranch, _, err := self.getCurrentBranchName()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -327,7 +384,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, _ := self.cmd.New(fmt.Sprintf("git merge-base %s %s", self.cmd.Quote(refName), self.cmd.Quote(baseBranch))).RunWithOutput()
|
||||
return ignoringWarnings(output), nil
|
||||
}
|
||||
|
||||
@@ -343,8 +400,12 @@ 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)
|
||||
func (self *CommitLoader) getFirstPushedCommit(refName string) (string, error) {
|
||||
output, err := self.cmd.
|
||||
New(
|
||||
fmt.Sprintf("git merge-base %s %s@{u}", self.cmd.Quote(refName), self.cmd.Quote(refName)),
|
||||
).
|
||||
RunWithOutput()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -353,29 +414,48 @@ func (c *CommitListBuilder) getFirstPushedCommit(refName string) (string, error)
|
||||
}
|
||||
|
||||
// getLog gets the git log.
|
||||
func (c *CommitListBuilder) getLogCmd(opts GetCommitsOptions) *exec.Cmd {
|
||||
func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj {
|
||||
limitFlag := ""
|
||||
if opts.Limit {
|
||||
limitFlag = "-300"
|
||||
limitFlag = " -300"
|
||||
}
|
||||
|
||||
filterFlag := ""
|
||||
if opts.FilterPath != "" {
|
||||
filterFlag = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(opts.FilterPath))
|
||||
filterFlag = fmt.Sprintf(" --follow -- %s", self.cmd.Quote(opts.FilterPath))
|
||||
}
|
||||
|
||||
return c.OSCommand.ExecutableFromString(
|
||||
config := self.UserConfig.Git.Log
|
||||
|
||||
orderFlag := "--" + config.Order
|
||||
allFlag := ""
|
||||
if opts.All {
|
||||
allFlag = " --all"
|
||||
}
|
||||
|
||||
return self.cmd.New(
|
||||
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",
|
||||
self.cmd.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 line != "" && strings.Split(line, " ")[0] != "gpg:"
|
||||
}
|
||||
212
pkg/commands/loaders/commits_test.go
Normal file
212
pkg/commands/loaders/commits_test.go
Normal file
@@ -0,0 +1,212 @@
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func NewDummyCommitLoader() *CommitLoader {
|
||||
cmn := utils.NewDummyCommon()
|
||||
|
||||
return &CommitLoader{
|
||||
Common: cmn,
|
||||
cmd: nil,
|
||||
getCurrentBranchName: func() (string, string, error) { return "master", "master", nil },
|
||||
getRebaseMode: func() (enums.RebaseMode, error) { return enums.REBASE_MODE_NONE, nil },
|
||||
dotGitDir: ".git",
|
||||
readFile: func(filename string) ([]byte, error) {
|
||||
return []byte(""), nil
|
||||
},
|
||||
walkFiles: func(root string, fn filepath.WalkFunc) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const commitsOutput = `0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield| (HEAD -> better-tests)|b21997d6b4cbdf84b149|better typing for rebase mode
|
||||
b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164|1640824515|Jesse Duffield| (origin/better-tests)|e94e8fc5b6fab4cb755f|fix logging
|
||||
e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c|1640823749|Jesse Duffield||d8084cd558925eb7c9c3|refactor
|
||||
d8084cd558925eb7c9c38afeed5725c21653ab90|1640821426|Jesse Duffield||65f910ebd85283b5cce9|WIP
|
||||
65f910ebd85283b5cce9bf67d03d3f1a9ea3813a|1640821275|Jesse Duffield||26c07b1ab33860a1a759|WIP
|
||||
26c07b1ab33860a1a7591a0638f9925ccf497ffa|1640750752|Jesse Duffield||3d4470a6c072208722e5|WIP
|
||||
3d4470a6c072208722e5ae9a54bcb9634959a1c5|1640748818|Jesse Duffield||053a66a7be3da43aacdc|WIP
|
||||
053a66a7be3da43aacdc7aa78e1fe757b82c4dd2|1640739815|Jesse Duffield||985fe482e806b172aea4|refactoring the config struct`
|
||||
|
||||
func TestGetCommits(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
runner oscommands.ICmdObjRunner
|
||||
expectedCommits []*models.Commit
|
||||
expectedError error
|
||||
rebaseMode enums.RebaseMode
|
||||
currentBranchName string
|
||||
opts GetCommitsOptions
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "should return no commits if there are none",
|
||||
rebaseMode: enums.REBASE_MODE_NONE,
|
||||
currentBranchName: "master",
|
||||
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git merge-base "HEAD" "HEAD"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
Expect(`git log "HEAD" --topo-order --oneline --pretty=format:"%H|%at|%aN|%d|%p|%s" --abbrev=20`, "", nil),
|
||||
|
||||
expectedCommits: []*models.Commit{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "should return commits if they are present",
|
||||
rebaseMode: enums.REBASE_MODE_NONE,
|
||||
currentBranchName: "master",
|
||||
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
// here it's seeing which commits are yet to be pushed
|
||||
Expect(`git merge-base "HEAD" "HEAD"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
|
||||
// here it's actually getting all the commits in a formatted form, one per line
|
||||
Expect(`git log "HEAD" --topo-order --oneline --pretty=format:"%H|%at|%aN|%d|%p|%s" --abbrev=20`, commitsOutput, nil).
|
||||
// here it's seeing where our branch diverged from the master branch so that we can mark that commit and parent commits as 'merged'
|
||||
Expect(`git merge-base "HEAD" "master"`, "26c07b1ab33860a1a7591a0638f9925ccf497ffa", nil),
|
||||
|
||||
expectedCommits: []*models.Commit{
|
||||
{
|
||||
Sha: "0eea75e8c631fba6b58135697835d58ba4c18dbc",
|
||||
Name: "better typing for rebase mode",
|
||||
Status: "unpushed",
|
||||
Action: "",
|
||||
Tags: []string{},
|
||||
ExtraInfo: "(HEAD -> better-tests)",
|
||||
Author: "Jesse Duffield",
|
||||
UnixTimestamp: 1640826609,
|
||||
Parents: []string{
|
||||
"b21997d6b4cbdf84b149",
|
||||
},
|
||||
},
|
||||
{
|
||||
Sha: "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164",
|
||||
Name: "fix logging",
|
||||
Status: "pushed",
|
||||
Action: "",
|
||||
Tags: []string{},
|
||||
ExtraInfo: "(origin/better-tests)",
|
||||
Author: "Jesse Duffield",
|
||||
UnixTimestamp: 1640824515,
|
||||
Parents: []string{
|
||||
"e94e8fc5b6fab4cb755f",
|
||||
},
|
||||
},
|
||||
{
|
||||
Sha: "e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c",
|
||||
Name: "refactor",
|
||||
Status: "pushed",
|
||||
Action: "",
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
Author: "Jesse Duffield",
|
||||
UnixTimestamp: 1640823749,
|
||||
Parents: []string{
|
||||
"d8084cd558925eb7c9c3",
|
||||
},
|
||||
},
|
||||
{
|
||||
Sha: "d8084cd558925eb7c9c38afeed5725c21653ab90",
|
||||
Name: "WIP",
|
||||
Status: "pushed",
|
||||
Action: "",
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
Author: "Jesse Duffield",
|
||||
UnixTimestamp: 1640821426,
|
||||
Parents: []string{
|
||||
"65f910ebd85283b5cce9",
|
||||
},
|
||||
},
|
||||
{
|
||||
Sha: "65f910ebd85283b5cce9bf67d03d3f1a9ea3813a",
|
||||
Name: "WIP",
|
||||
Status: "pushed",
|
||||
Action: "",
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
Author: "Jesse Duffield",
|
||||
UnixTimestamp: 1640821275,
|
||||
Parents: []string{
|
||||
"26c07b1ab33860a1a759",
|
||||
},
|
||||
},
|
||||
{
|
||||
Sha: "26c07b1ab33860a1a7591a0638f9925ccf497ffa",
|
||||
Name: "WIP",
|
||||
Status: "merged",
|
||||
Action: "",
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
Author: "Jesse Duffield",
|
||||
UnixTimestamp: 1640750752,
|
||||
Parents: []string{
|
||||
"3d4470a6c072208722e5",
|
||||
},
|
||||
},
|
||||
{
|
||||
Sha: "3d4470a6c072208722e5ae9a54bcb9634959a1c5",
|
||||
Name: "WIP",
|
||||
Status: "merged",
|
||||
Action: "",
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
Author: "Jesse Duffield",
|
||||
UnixTimestamp: 1640748818,
|
||||
Parents: []string{
|
||||
"053a66a7be3da43aacdc",
|
||||
},
|
||||
},
|
||||
{
|
||||
Sha: "053a66a7be3da43aacdc7aa78e1fe757b82c4dd2",
|
||||
Name: "refactoring the config struct",
|
||||
Status: "merged",
|
||||
Action: "",
|
||||
Tags: []string{},
|
||||
ExtraInfo: "",
|
||||
Author: "Jesse Duffield",
|
||||
UnixTimestamp: 1640739815,
|
||||
Parents: []string{
|
||||
"985fe482e806b172aea4",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
t.Run(scenario.testName, func(t *testing.T) {
|
||||
builder := &CommitLoader{
|
||||
Common: utils.NewDummyCommon(),
|
||||
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
|
||||
getCurrentBranchName: func() (string, string, error) {
|
||||
return scenario.currentBranchName, scenario.currentBranchName, nil
|
||||
},
|
||||
getRebaseMode: func() (enums.RebaseMode, error) { return scenario.rebaseMode, nil },
|
||||
dotGitDir: ".git",
|
||||
readFile: func(filename string) ([]byte, error) {
|
||||
return []byte(""), nil
|
||||
},
|
||||
walkFiles: func(root string, fn filepath.WalkFunc) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
commits, err := builder.GetCommits(scenario.opts)
|
||||
|
||||
assert.Equal(t, scenario.expectedCommits, commits)
|
||||
assert.Equal(t, scenario.expectedError, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
134
pkg/commands/loaders/files.go
Normal file
134
pkg/commands/loaders/files.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type FileLoader struct {
|
||||
*common.Common
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
gitConfig git_config.IGitConfig
|
||||
getFileType func(string) string
|
||||
}
|
||||
|
||||
func NewFileLoader(cmn *common.Common, cmd oscommands.ICmdObjBuilder, gitConfig git_config.IGitConfig) *FileLoader {
|
||||
return &FileLoader{
|
||||
Common: cmn,
|
||||
cmd: cmd,
|
||||
gitConfig: gitConfig,
|
||||
getFileType: oscommands.FileType,
|
||||
}
|
||||
}
|
||||
|
||||
type GetStatusFileOptions struct {
|
||||
NoRenames bool
|
||||
}
|
||||
|
||||
func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
|
||||
// check if config wants us ignoring untracked files
|
||||
untrackedFilesSetting := self.gitConfig.Get("status.showUntrackedFiles")
|
||||
|
||||
if untrackedFilesSetting == "" {
|
||||
untrackedFilesSetting = "all"
|
||||
}
|
||||
untrackedFilesArg := fmt.Sprintf("--untracked-files=%s", untrackedFilesSetting)
|
||||
|
||||
statuses, err := self.GitStatus(GitStatusOptions{NoRenames: opts.NoRenames, UntrackedFilesArg: untrackedFilesArg})
|
||||
if err != nil {
|
||||
self.Log.Error(err)
|
||||
}
|
||||
files := []*models.File{}
|
||||
|
||||
for _, status := range statuses {
|
||||
if strings.HasPrefix(status.StatusString, "warning") {
|
||||
self.Log.Warningf("warning when calling git status: %s", status.StatusString)
|
||||
continue
|
||||
}
|
||||
change := status.Change
|
||||
stagedChange := change[0:1]
|
||||
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)
|
||||
|
||||
file := &models.File{
|
||||
Name: status.Name,
|
||||
PreviousName: status.PreviousName,
|
||||
DisplayString: status.StatusString,
|
||||
HasStagedChanges: !hasNoStagedChanges,
|
||||
HasUnstagedChanges: unstagedChange != " ",
|
||||
Tracked: !untracked,
|
||||
Deleted: unstagedChange == "D" || stagedChange == "D",
|
||||
Added: unstagedChange == "A" || untracked,
|
||||
HasMergeConflicts: hasMergeConflicts,
|
||||
HasInlineMergeConflicts: hasInlineMergeConflicts,
|
||||
Type: self.getFileType(status.Name),
|
||||
ShortStatus: change,
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
// GitStatus returns the file status of the repo
|
||||
type GitStatusOptions struct {
|
||||
NoRenames bool
|
||||
UntrackedFilesArg string
|
||||
}
|
||||
|
||||
type FileStatus struct {
|
||||
StatusString string
|
||||
Change string // ??, MM, AM, ...
|
||||
Name string
|
||||
PreviousName string
|
||||
}
|
||||
|
||||
func (c *FileLoader) GitStatus(opts GitStatusOptions) ([]FileStatus, error) {
|
||||
noRenamesFlag := ""
|
||||
if opts.NoRenames {
|
||||
noRenamesFlag = " --no-renames"
|
||||
}
|
||||
|
||||
statusLines, err := c.cmd.New(fmt.Sprintf("git status %s --porcelain -z%s", opts.UntrackedFilesArg, noRenamesFlag)).RunWithOutput()
|
||||
if err != nil {
|
||||
return []FileStatus{}, err
|
||||
}
|
||||
|
||||
splitLines := strings.Split(statusLines, "\x00")
|
||||
response := []FileStatus{}
|
||||
|
||||
for i := 0; i < len(splitLines); i++ {
|
||||
original := splitLines[i]
|
||||
|
||||
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 response, nil
|
||||
}
|
||||
203
pkg/commands/loaders/files_test.go
Normal file
203
pkg/commands/loaders/files_test.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGitCommandGetStatusFiles(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
runner oscommands.ICmdObjRunner
|
||||
expectedFiles []*models.File
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"No files found",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(`git status --untracked-files=yes --porcelain -z`, "", nil),
|
||||
[]*models.File{},
|
||||
},
|
||||
{
|
||||
"Several files found",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(
|
||||
`git status --untracked-files=yes --porcelain -z`,
|
||||
"MM file1.txt\x00A file3.txt\x00AM file2.txt\x00?? file4.txt\x00UU file5.txt",
|
||||
nil,
|
||||
),
|
||||
[]*models.File{
|
||||
{
|
||||
Name: "file1.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Added: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "MM file1.txt",
|
||||
Type: "file",
|
||||
ShortStatus: "MM",
|
||||
},
|
||||
{
|
||||
Name: "file3.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: false,
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "A file3.txt",
|
||||
Type: "file",
|
||||
ShortStatus: "A ",
|
||||
},
|
||||
{
|
||||
Name: "file2.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "AM file2.txt",
|
||||
Type: "file",
|
||||
ShortStatus: "AM",
|
||||
},
|
||||
{
|
||||
Name: "file4.txt",
|
||||
HasStagedChanges: false,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "?? file4.txt",
|
||||
Type: "file",
|
||||
ShortStatus: "??",
|
||||
},
|
||||
{
|
||||
Name: "file5.txt",
|
||||
HasStagedChanges: false,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Added: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: true,
|
||||
HasInlineMergeConflicts: true,
|
||||
DisplayString: "UU file5.txt",
|
||||
Type: "file",
|
||||
ShortStatus: "UU",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"File with new line char",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(`git status --untracked-files=yes --porcelain -z`, "MM a\nb.txt", nil),
|
||||
[]*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: "file",
|
||||
ShortStatus: "MM",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"Renamed files",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(
|
||||
`git status --untracked-files=yes --porcelain -z`,
|
||||
"R after1.txt\x00before1.txt\x00RM after2.txt\x00before2.txt",
|
||||
nil,
|
||||
),
|
||||
[]*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: "file",
|
||||
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: "file",
|
||||
ShortStatus: "RM",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"File with arrow in name",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(
|
||||
`git status --untracked-files=yes --porcelain -z`,
|
||||
`?? a -> b.txt`,
|
||||
nil,
|
||||
),
|
||||
[]*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: "file",
|
||||
ShortStatus: "??",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
cmd := oscommands.NewDummyCmdObjBuilder(s.runner)
|
||||
gitConfig := git_config.NewFakeGitConfig(map[string]string{"status.showUntrackedFiles": "yes"})
|
||||
|
||||
loader := &FileLoader{
|
||||
Common: utils.NewDummyCommon(),
|
||||
cmd: cmd,
|
||||
gitConfig: gitConfig,
|
||||
getFileType: func(string) string { return "file" },
|
||||
}
|
||||
|
||||
assert.EqualValues(t, s.expectedFiles, loader.GetStatusFiles(GetStatusFileOptions{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,50 @@
|
||||
package commands
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
)
|
||||
|
||||
type ReflogCommitLoader struct {
|
||||
*common.Common
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
}
|
||||
|
||||
func NewReflogCommitLoader(common *common.Common, cmd oscommands.ICmdObjBuilder) *ReflogCommitLoader {
|
||||
return &ReflogCommitLoader{
|
||||
Common: common,
|
||||
cmd: cmd,
|
||||
}
|
||||
}
|
||||
|
||||
// GetReflogCommits only returns the new reflog commits since the given lastReflogCommit
|
||||
// if none is passed (i.e. it's value is nil) then we get all the reflog commits
|
||||
func (c *GitCommand) GetReflogCommits(lastReflogCommit *models.Commit, filterPath string) ([]*models.Commit, bool, error) {
|
||||
func (self *ReflogCommitLoader) 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))
|
||||
filterPathArg = fmt.Sprintf(" --follow -- %s", self.cmd.Quote(filterPath))
|
||||
}
|
||||
|
||||
cmd := c.OSCommand.ExecutableFromString(fmt.Sprintf("git reflog --abbrev=20 --date=unix %s", filterPathArg))
|
||||
cmdObj := self.cmd.New(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 {
|
||||
err := cmdObj.RunAndProcessLines(func(line string) (bool, error) {
|
||||
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,4 +1,4 @@
|
||||
package commands
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -6,18 +6,37 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
gogit "github.com/jesseduffield/go-git/v5"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
)
|
||||
|
||||
func (c *GitCommand) GetRemotes() ([]*models.Remote, error) {
|
||||
// get remote branches
|
||||
unescaped := "git branch -r"
|
||||
remoteBranchesStr, err := c.OSCommand.RunCommandWithOutput(unescaped)
|
||||
type RemoteLoader struct {
|
||||
*common.Common
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
getGoGitRemotes func() ([]*gogit.Remote, error)
|
||||
}
|
||||
|
||||
func NewRemoteLoader(
|
||||
common *common.Common,
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
getGoGitRemotes func() ([]*gogit.Remote, error),
|
||||
) *RemoteLoader {
|
||||
return &RemoteLoader{
|
||||
Common: common,
|
||||
cmd: cmd,
|
||||
getGoGitRemotes: getGoGitRemotes,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *RemoteLoader) GetRemotes() ([]*models.Remote, error) {
|
||||
remoteBranchesStr, err := self.cmd.New("git branch -r").RunWithOutput()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
goGitRemotes, err := c.Repo.Remotes()
|
||||
goGitRemotes, err := self.getGoGitRemotes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -27,7 +46,7 @@ func (c *GitCommand) GetRemotes() ([]*models.Remote, error) {
|
||||
for i, goGitRemote := range goGitRemotes {
|
||||
remoteName := goGitRemote.Config().Name
|
||||
|
||||
re := regexp.MustCompile(fmt.Sprintf(`%s\/([\S]+)`, remoteName))
|
||||
re := regexp.MustCompile(fmt.Sprintf(`(?m)^\s*%s\/([\S]+)`, remoteName))
|
||||
matches := re.FindAllStringSubmatch(remoteBranchesStr, -1)
|
||||
branches := make([]*models.RemoteBranch, len(matches))
|
||||
for j, match := range matches {
|
||||
80
pkg/commands/loaders/stash.go
Normal file
80
pkg/commands/loaders/stash.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type StashLoader struct {
|
||||
*common.Common
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
}
|
||||
|
||||
func NewStashLoader(
|
||||
common *common.Common,
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
) *StashLoader {
|
||||
return &StashLoader{
|
||||
Common: common,
|
||||
cmd: cmd,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *StashLoader) GetStashEntries(filterPath string) []*models.StashEntry {
|
||||
if filterPath == "" {
|
||||
return self.getUnfilteredStashEntries()
|
||||
}
|
||||
|
||||
rawString, err := self.cmd.New("git stash list --name-only").RunWithOutput()
|
||||
if err != nil {
|
||||
return self.getUnfilteredStashEntries()
|
||||
}
|
||||
stashEntries := []*models.StashEntry{}
|
||||
var currentStashEntry *models.StashEntry
|
||||
lines := utils.SplitLines(rawString)
|
||||
isAStash := func(line string) bool { return strings.HasPrefix(line, "stash@{") }
|
||||
re := regexp.MustCompile(`stash@\{(\d+)\}`)
|
||||
|
||||
outer:
|
||||
for i := 0; i < len(lines); i++ {
|
||||
if !isAStash(lines[i]) {
|
||||
continue
|
||||
}
|
||||
match := re.FindStringSubmatch(lines[i])
|
||||
idx, err := strconv.Atoi(match[1])
|
||||
if err != nil {
|
||||
return self.getUnfilteredStashEntries()
|
||||
}
|
||||
currentStashEntry = self.stashEntryFromLine(lines[i], idx)
|
||||
for i+1 < len(lines) && !isAStash(lines[i+1]) {
|
||||
i++
|
||||
if lines[i] == filterPath {
|
||||
stashEntries = append(stashEntries, currentStashEntry)
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
}
|
||||
return stashEntries
|
||||
}
|
||||
|
||||
func (self *StashLoader) getUnfilteredStashEntries() []*models.StashEntry {
|
||||
rawString, _ := self.cmd.New("git stash list --pretty='%gs'").RunWithOutput()
|
||||
stashEntries := []*models.StashEntry{}
|
||||
for i, line := range utils.SplitLines(rawString) {
|
||||
stashEntries = append(stashEntries, self.stashEntryFromLine(line, i))
|
||||
}
|
||||
return stashEntries
|
||||
}
|
||||
|
||||
func (c *StashLoader) stashEntryFromLine(line string, index int) *models.StashEntry {
|
||||
return &models.StashEntry{
|
||||
Name: line,
|
||||
Index: index,
|
||||
}
|
||||
}
|
||||
59
pkg/commands/loaders/stash_test.go
Normal file
59
pkg/commands/loaders/stash_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGitCommandGetStashEntries(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
filterPath string
|
||||
runner oscommands.ICmdObjRunner
|
||||
expectedStashEntries []*models.StashEntry
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"No stash entries found",
|
||||
"",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(`git stash list --pretty='%gs'`, "", nil),
|
||||
[]*models.StashEntry{},
|
||||
},
|
||||
{
|
||||
"Several stash entries found",
|
||||
"",
|
||||
oscommands.NewFakeRunner(t).
|
||||
Expect(
|
||||
`git stash list --pretty='%gs'`,
|
||||
"WIP on add-pkg-commands-test: 55c6af2 increase parallel build\nWIP on master: bb86a3f update github template",
|
||||
nil,
|
||||
),
|
||||
[]*models.StashEntry{
|
||||
{
|
||||
Index: 0,
|
||||
Name: "WIP on add-pkg-commands-test: 55c6af2 increase parallel build",
|
||||
},
|
||||
{
|
||||
Index: 1,
|
||||
Name: "WIP on master: bb86a3f update github template",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
cmd := oscommands.NewDummyCmdObjBuilder(s.runner)
|
||||
|
||||
loader := NewStashLoader(utils.NewDummyCommon(), cmd)
|
||||
|
||||
assert.EqualValues(t, s.expectedStashEntries, loader.GetStashEntries(""))
|
||||
})
|
||||
}
|
||||
}
|
||||
51
pkg/commands/loaders/tags.go
Normal file
51
pkg/commands/loaders/tags.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type TagLoader struct {
|
||||
*common.Common
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
}
|
||||
|
||||
func NewTagLoader(
|
||||
common *common.Common,
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
) *TagLoader {
|
||||
return &TagLoader{
|
||||
Common: common,
|
||||
cmd: cmd,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *TagLoader) GetTags() ([]*models.Tag, error) {
|
||||
// get remote branches, sorted by creation date (descending)
|
||||
// see: https://git-scm.com/docs/git-tag#Documentation/git-tag.txt---sortltkeygt
|
||||
remoteBranchesStr, err := self.cmd.New(`git tag --list --sort=-creatordate`).RunWithOutput()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
content := utils.TrimTrailingNewline(remoteBranchesStr)
|
||||
if content == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
split := strings.Split(content, "\n")
|
||||
|
||||
// first step is to get our remotes from go-git
|
||||
tags := make([]*models.Tag, len(split))
|
||||
for i, tagName := range split {
|
||||
tags[i] = &models.Tag{
|
||||
Name: tagName,
|
||||
}
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
)
|
||||
|
||||
// GetFilesInDiff get the specified commit files
|
||||
func (c *GitCommand) GetFilesInDiff(from string, to string, reverse bool) ([]*models.CommitFile, error) {
|
||||
reverseFlag := ""
|
||||
if reverse {
|
||||
reverseFlag = " -R "
|
||||
}
|
||||
|
||||
filenames, err := c.RunCommandWithOutput("git diff --submodule --no-ext-diff --name-status -z --no-renames %s %s %s", reverseFlag, from, to)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.getCommitFilesFromFilenames(filenames), nil
|
||||
}
|
||||
|
||||
// filenames string is something like "file1\nfile2\nfile3"
|
||||
func (c *GitCommand) getCommitFilesFromFilenames(filenames string) []*models.CommitFile {
|
||||
commitFiles := make([]*models.CommitFile, 0)
|
||||
|
||||
lines := strings.Split(strings.TrimRight(filenames, "\x00"), "\x00")
|
||||
n := len(lines)
|
||||
for i := 0; i < n-1; i += 2 {
|
||||
// typical result looks like 'A my_file' meaning my_file was added
|
||||
changeStatus := lines[i]
|
||||
name := lines[i+1]
|
||||
|
||||
commitFiles = append(commitFiles, &models.CommitFile{
|
||||
Name: name,
|
||||
ChangeStatus: changeStatus,
|
||||
})
|
||||
}
|
||||
|
||||
return commitFiles
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// NewDummyCommitListBuilder creates a new dummy CommitListBuilder for testing
|
||||
func NewDummyCommitListBuilder() *CommitListBuilder {
|
||||
osCommand := oscommands.NewDummyOSCommand()
|
||||
|
||||
return &CommitListBuilder{
|
||||
Log: utils.NewDummyLog(),
|
||||
GitCommand: NewDummyGitCommandWithOSCommand(osCommand),
|
||||
OSCommand: osCommand,
|
||||
Tr: i18n.NewTranslationSet(utils.NewDummyLog()),
|
||||
}
|
||||
}
|
||||
|
||||
// TestCommitListBuilderGetMergeBase is a function.
|
||||
func TestCommitListBuilderGetMergeBase(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(string, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"swallows an error if the call to merge-base returns an error",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
|
||||
switch args[0] {
|
||||
case "symbolic-ref":
|
||||
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
|
||||
return secureexec.Command("echo", "master")
|
||||
case "merge-base":
|
||||
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
|
||||
return secureexec.Command("test")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
func(output string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "", output)
|
||||
},
|
||||
},
|
||||
{
|
||||
"returns the commit when master",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
|
||||
switch args[0] {
|
||||
case "symbolic-ref":
|
||||
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
|
||||
return secureexec.Command("echo", "master")
|
||||
case "merge-base":
|
||||
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
|
||||
return secureexec.Command("echo", "blah")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
func(output string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "blah", output)
|
||||
},
|
||||
},
|
||||
{
|
||||
"checks against develop when a feature branch",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
|
||||
switch args[0] {
|
||||
case "symbolic-ref":
|
||||
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
|
||||
return secureexec.Command("echo", "feature/test")
|
||||
case "merge-base":
|
||||
assert.EqualValues(t, []string{"merge-base", "HEAD", "develop"}, args)
|
||||
return secureexec.Command("echo", "blah")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
func(output string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "blah", output)
|
||||
},
|
||||
},
|
||||
{
|
||||
"bubbles up error if there is one",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return secureexec.Command("test")
|
||||
},
|
||||
func(output string, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "", output)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
c := NewDummyCommitListBuilder()
|
||||
c.OSCommand.SetCommand(s.command)
|
||||
s.test(c.getMergeBase("HEAD"))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
const RENAME_SEPARATOR = " -> "
|
||||
|
||||
// GetStatusFiles git status files
|
||||
type GetStatusFileOptions struct {
|
||||
NoRenames bool
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
|
||||
// check if config wants us ignoring untracked files
|
||||
untrackedFilesSetting := c.GetConfigValue("status.showUntrackedFiles")
|
||||
|
||||
if untrackedFilesSetting == "" {
|
||||
untrackedFilesSetting = "all"
|
||||
}
|
||||
untrackedFilesArg := fmt.Sprintf("--untracked-files=%s", untrackedFilesSetting)
|
||||
|
||||
statusOutput, err := c.GitStatus(GitStatusOptions{NoRenames: opts.NoRenames, UntrackedFilesArg: untrackedFilesArg})
|
||||
if err != nil {
|
||||
c.Log.Error(err)
|
||||
}
|
||||
statusStrings := utils.SplitLines(statusOutput)
|
||||
files := []*models.File{}
|
||||
|
||||
for _, statusString := range statusStrings {
|
||||
if strings.HasPrefix(statusString, "warning") {
|
||||
c.Log.Warningf("warning when calling git status: %s", statusString)
|
||||
continue
|
||||
}
|
||||
change := statusString[0:2]
|
||||
stagedChange := change[0:1]
|
||||
unstagedChange := statusString[1:2]
|
||||
name := statusString[3:]
|
||||
untracked := utils.IncludesString([]string{"??", "A ", "AM"}, change)
|
||||
hasNoStagedChanges := utils.IncludesString([]string{" ", "U", "?"}, stagedChange)
|
||||
hasMergeConflicts := utils.IncludesString([]string{"DD", "AA", "UU", "AU", "UA", "UD", "DU"}, change)
|
||||
hasInlineMergeConflicts := utils.IncludesString([]string{"UU", "AA"}, change)
|
||||
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,
|
||||
HasStagedChanges: !hasNoStagedChanges,
|
||||
HasUnstagedChanges: unstagedChange != " ",
|
||||
Tracked: !untracked,
|
||||
Deleted: unstagedChange == "D" || stagedChange == "D",
|
||||
Added: unstagedChange == "A" || untracked,
|
||||
HasMergeConflicts: hasMergeConflicts,
|
||||
HasInlineMergeConflicts: hasInlineMergeConflicts,
|
||||
Type: c.OSCommand.FileType(name),
|
||||
ShortStatus: change,
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
// GitStatus returns the plaintext short status of the repo
|
||||
type GitStatusOptions struct {
|
||||
NoRenames bool
|
||||
UntrackedFilesArg string
|
||||
}
|
||||
|
||||
func (c *GitCommand) GitStatus(opts GitStatusOptions) (string, error) {
|
||||
noRenamesFlag := ""
|
||||
if opts.NoRenames {
|
||||
noRenamesFlag = "--no-renames"
|
||||
}
|
||||
|
||||
statusLines, err := c.RunCommandWithOutput("git status %s --porcelain -z %s", opts.UntrackedFilesArg, noRenamesFlag)
|
||||
if err != nil {
|
||||
return "", 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++ {
|
||||
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:]...)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(splitLines, "\n"), nil
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandGetStatusFiles is a function.
|
||||
func TestGitCommandGetStatusFiles(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func([]*models.File)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"No files found",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(files []*models.File) {
|
||||
assert.Len(t, files, 0)
|
||||
},
|
||||
},
|
||||
{
|
||||
"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",
|
||||
)
|
||||
},
|
||||
func(files []*models.File) {
|
||||
assert.Len(t, files, 5)
|
||||
|
||||
expected := []*models.File{
|
||||
{
|
||||
Name: "file1.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Added: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "MM file1.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "MM",
|
||||
},
|
||||
{
|
||||
Name: "file3.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: false,
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "A file3.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "A ",
|
||||
},
|
||||
{
|
||||
Name: "file2.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "AM file2.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "AM",
|
||||
},
|
||||
{
|
||||
Name: "file4.txt",
|
||||
HasStagedChanges: false,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "?? file4.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "??",
|
||||
},
|
||||
{
|
||||
Name: "file5.txt",
|
||||
HasStagedChanges: false,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Added: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: true,
|
||||
HasInlineMergeConflicts: true,
|
||||
DisplayString: "UU file5.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "UU",
|
||||
},
|
||||
}
|
||||
|
||||
assert.EqualValues(t, expected, files)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
|
||||
s.test(gitCmd.GetStatusFiles(GetStatusFileOptions{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func (c *GitCommand) getUnfilteredStashEntries() []*models.StashEntry {
|
||||
unescaped := "git stash list --pretty='%gs'"
|
||||
rawString, _ := c.OSCommand.RunCommandWithOutput(unescaped)
|
||||
stashEntries := []*models.StashEntry{}
|
||||
for i, line := range utils.SplitLines(rawString) {
|
||||
stashEntries = append(stashEntries, stashEntryFromLine(line, i))
|
||||
}
|
||||
return stashEntries
|
||||
}
|
||||
|
||||
// GetStashEntries stash entries
|
||||
func (c *GitCommand) GetStashEntries(filterPath string) []*models.StashEntry {
|
||||
if filterPath == "" {
|
||||
return c.getUnfilteredStashEntries()
|
||||
}
|
||||
|
||||
rawString, err := c.RunCommandWithOutput("git stash list --name-only")
|
||||
if err != nil {
|
||||
return c.getUnfilteredStashEntries()
|
||||
}
|
||||
stashEntries := []*models.StashEntry{}
|
||||
var currentStashEntry *models.StashEntry
|
||||
lines := utils.SplitLines(rawString)
|
||||
isAStash := func(line string) bool { return strings.HasPrefix(line, "stash@{") }
|
||||
re := regexp.MustCompile(`stash@\{(\d+)\}`)
|
||||
|
||||
outer:
|
||||
for i := 0; i < len(lines); i++ {
|
||||
if !isAStash(lines[i]) {
|
||||
continue
|
||||
}
|
||||
match := re.FindStringSubmatch(lines[i])
|
||||
idx, err := strconv.Atoi(match[1])
|
||||
if err != nil {
|
||||
return c.getUnfilteredStashEntries()
|
||||
}
|
||||
currentStashEntry = stashEntryFromLine(lines[i], idx)
|
||||
for i+1 < len(lines) && !isAStash(lines[i+1]) {
|
||||
i++
|
||||
if lines[i] == filterPath {
|
||||
stashEntries = append(stashEntries, currentStashEntry)
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
}
|
||||
return stashEntries
|
||||
}
|
||||
|
||||
func stashEntryFromLine(line string, index int) *models.StashEntry {
|
||||
return &models.StashEntry{
|
||||
Name: line,
|
||||
Index: index,
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandGetStashEntries is a function.
|
||||
func TestGitCommandGetStashEntries(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func([]*models.StashEntry)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"No stash entries found",
|
||||
func(string, ...string) *exec.Cmd {
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(entries []*models.StashEntry) {
|
||||
assert.Len(t, entries, 0)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Several stash entries found",
|
||||
func(string, ...string) *exec.Cmd {
|
||||
return secureexec.Command("echo", "WIP on add-pkg-commands-test: 55c6af2 increase parallel build\nWIP on master: bb86a3f update github template")
|
||||
},
|
||||
func(entries []*models.StashEntry) {
|
||||
expected := []*models.StashEntry{
|
||||
{
|
||||
Index: 0,
|
||||
Name: "WIP on add-pkg-commands-test: 55c6af2 increase parallel build",
|
||||
},
|
||||
{
|
||||
Index: 1,
|
||||
Name: "WIP on master: bb86a3f update github template",
|
||||
},
|
||||
}
|
||||
|
||||
assert.Len(t, entries, 2)
|
||||
assert.EqualValues(t, expected, entries)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
|
||||
s.test(gitCmd.GetStashEntries(""))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
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`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
content := utils.TrimTrailingNewline(remoteBranchesStr)
|
||||
if content == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
split := strings.Split(content, "\n")
|
||||
|
||||
// first step is to get our remotes from go-git
|
||||
tags := make([]*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
|
||||
}
|
||||
}
|
||||
|
||||
71
pkg/commands/oscommands/cmd_obj.go
Normal file
71
pkg/commands/oscommands/cmd_obj.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// A command object is a general way to represent a command to be run on the
|
||||
// command line.
|
||||
type ICmdObj interface {
|
||||
GetCmd() *exec.Cmd
|
||||
// outputs string representation of command. Note that if the command was built
|
||||
// using NewFromArgs, the output won't be quite the same as what you would type
|
||||
// into a terminal e.g. 'sh -c git commit' as opposed to 'sh -c "git commit"'
|
||||
ToString() string
|
||||
AddEnvVars(...string) ICmdObj
|
||||
GetEnvVars() []string
|
||||
|
||||
// runs the command and returns an error if any
|
||||
Run() error
|
||||
// runs the command and returns the output as a string, and an error if any
|
||||
RunWithOutput() (string, error)
|
||||
// runs the command and runs a callback function on each line of the output. If the callback returns true for the boolean value, we kill the process and return.
|
||||
RunAndProcessLines(onLine func(line string) (bool, error)) error
|
||||
|
||||
// logs command
|
||||
Log() ICmdObj
|
||||
}
|
||||
|
||||
type CmdObj struct {
|
||||
cmdStr string
|
||||
cmd *exec.Cmd
|
||||
|
||||
runner ICmdObjRunner
|
||||
logCommand func(ICmdObj)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (self *CmdObj) GetEnvVars() []string {
|
||||
return self.cmd.Env
|
||||
}
|
||||
|
||||
func (self *CmdObj) Log() ICmdObj {
|
||||
self.logCommand(self)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *CmdObj) Run() error {
|
||||
return self.runner.Run(self)
|
||||
}
|
||||
|
||||
func (self *CmdObj) RunWithOutput() (string, error) {
|
||||
return self.runner.RunWithOutput(self)
|
||||
}
|
||||
|
||||
func (self *CmdObj) RunAndProcessLines(onLine func(line string) (bool, error)) error {
|
||||
return self.runner.RunAndProcessLines(self, onLine)
|
||||
}
|
||||
68
pkg/commands/oscommands/cmd_obj_builder.go
Normal file
68
pkg/commands/oscommands/cmd_obj_builder.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/mgutz/str"
|
||||
)
|
||||
|
||||
type ICmdObjBuilder interface {
|
||||
// New returns a new command object based on the string provided
|
||||
New(cmdStr string) ICmdObj
|
||||
// NewShell takes a string like `git commit` and returns an executable shell command for it e.g. `sh -c 'git commit'`
|
||||
NewShell(commandStr string) ICmdObj
|
||||
// NewFromArgs takes a slice of strings like []string{"git", "commit"} and returns a new command object. This can be useful when you don't want to worry about whitespace and quoting and stuff.
|
||||
NewFromArgs(args []string) ICmdObj
|
||||
// Quote wraps a string in quotes with any necessary escaping applied. The reason for bundling this up with the other methods in this interface is that we basically always need to make use of this when creating new command objects.
|
||||
Quote(str string) string
|
||||
}
|
||||
|
||||
type CmdObjBuilder struct {
|
||||
runner ICmdObjRunner
|
||||
logCmdObj func(ICmdObj)
|
||||
platform *Platform
|
||||
}
|
||||
|
||||
// poor man's version of explicitly saying that struct X implements interface Y
|
||||
var _ ICmdObjBuilder = &CmdObjBuilder{}
|
||||
|
||||
func (self *CmdObjBuilder) New(cmdStr string) ICmdObj {
|
||||
args := str.ToArgv(cmdStr)
|
||||
cmd := secureexec.Command(args[0], args[1:]...)
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
return &CmdObj{
|
||||
cmdStr: cmdStr,
|
||||
cmd: cmd,
|
||||
runner: self.runner,
|
||||
logCommand: self.logCmdObj,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *CmdObjBuilder) NewFromArgs(args []string) ICmdObj {
|
||||
cmd := secureexec.Command(args[0], args[1:]...)
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
return &CmdObj{
|
||||
cmdStr: strings.Join(args, " "),
|
||||
cmd: cmd,
|
||||
runner: self.runner,
|
||||
logCommand: self.logCmdObj,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *CmdObjBuilder) NewShell(commandStr string) ICmdObj {
|
||||
return self.NewFromArgs([]string{self.platform.Shell, self.platform.ShellArg, commandStr})
|
||||
}
|
||||
|
||||
func (self *CmdObjBuilder) CloneWithNewRunner(decorate func(ICmdObjRunner) ICmdObjRunner) *CmdObjBuilder {
|
||||
decoratedRunner := decorate(self.runner)
|
||||
|
||||
return &CmdObjBuilder{
|
||||
runner: decoratedRunner,
|
||||
logCmdObj: self.logCmdObj,
|
||||
platform: self.platform,
|
||||
}
|
||||
}
|
||||
79
pkg/commands/oscommands/cmd_obj_runner.go
Normal file
79
pkg/commands/oscommands/cmd_obj_runner.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type ICmdObjRunner interface {
|
||||
Run(cmdObj ICmdObj) error
|
||||
RunWithOutput(cmdObj ICmdObj) (string, error)
|
||||
RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error
|
||||
}
|
||||
|
||||
type cmdObjRunner struct {
|
||||
log *logrus.Entry
|
||||
logCmdObj func(ICmdObj)
|
||||
}
|
||||
|
||||
var _ ICmdObjRunner = &cmdObjRunner{}
|
||||
|
||||
func (self *cmdObjRunner) Run(cmdObj ICmdObj) error {
|
||||
_, err := self.RunWithOutput(cmdObj)
|
||||
return err
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
|
||||
self.logCmdObj(cmdObj)
|
||||
output, err := sanitisedCommandOutput(cmdObj.GetCmd().CombinedOutput())
|
||||
if err != nil {
|
||||
self.log.WithField("command", cmdObj.ToString()).Error(output)
|
||||
}
|
||||
return output, err
|
||||
}
|
||||
|
||||
func (self *cmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error {
|
||||
cmd := cmdObj.GetCmd()
|
||||
stdoutPipe, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(stdoutPipe)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
stop, err := onLine(line)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if stop {
|
||||
_ = cmd.Process.Kill()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
_ = cmd.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func sanitisedCommandOutput(output []byte, err error) (string, error) {
|
||||
outputString := string(output)
|
||||
if err != nil {
|
||||
// errors like 'exit status 1' are not very useful so we'll create an error
|
||||
// from the combined output
|
||||
if outputString == "" {
|
||||
return "", utils.WrapError(err)
|
||||
}
|
||||
return outputString, errors.New(outputString)
|
||||
}
|
||||
return outputString, nil
|
||||
}
|
||||
@@ -1,11 +1,31 @@
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// NewDummyOSCommand creates a new dummy OSCommand for testing
|
||||
func NewDummyOSCommand() *OSCommand {
|
||||
return NewOSCommand(utils.NewDummyLog(), config.NewDummyAppConfig())
|
||||
return NewOSCommand(utils.NewDummyCommon())
|
||||
}
|
||||
|
||||
func NewDummyCmdObjBuilder(runner ICmdObjRunner) *CmdObjBuilder {
|
||||
return &CmdObjBuilder{
|
||||
runner: runner,
|
||||
logCmdObj: func(ICmdObj) {},
|
||||
platform: &Platform{
|
||||
OS: "darwin",
|
||||
Shell: "bash",
|
||||
ShellArg: "-c",
|
||||
OpenCommand: "open {{filename}}",
|
||||
OpenLinkCommand: "open {{link}}",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func NewDummyOSCommandWithRunner(runner *FakeCmdObjRunner) *OSCommand {
|
||||
osCommand := NewOSCommand(utils.NewDummyCommon())
|
||||
osCommand.Cmd = NewDummyCmdObjBuilder(runner)
|
||||
|
||||
return osCommand
|
||||
}
|
||||
|
||||
153
pkg/commands/oscommands/exec_live.go
Normal file
153
pkg/commands/oscommands/exec_live.go
Normal file
@@ -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
|
||||
|
||||
// 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)
|
||||
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. 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
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
102
pkg/commands/oscommands/fake_cmd_obj_runner.go
Normal file
102
pkg/commands/oscommands/fake_cmd_obj_runner.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// for use in testing
|
||||
|
||||
type FakeCmdObjRunner struct {
|
||||
t *testing.T
|
||||
expectedCmds []func(ICmdObj) (string, error)
|
||||
expectedCmdIndex int
|
||||
}
|
||||
|
||||
var _ ICmdObjRunner = &FakeCmdObjRunner{}
|
||||
|
||||
func NewFakeRunner(t *testing.T) *FakeCmdObjRunner {
|
||||
return &FakeCmdObjRunner{t: t}
|
||||
}
|
||||
|
||||
func (self *FakeCmdObjRunner) Run(cmdObj ICmdObj) error {
|
||||
_, err := self.RunWithOutput(cmdObj)
|
||||
return err
|
||||
}
|
||||
|
||||
func (self *FakeCmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
|
||||
if self.expectedCmdIndex > len(self.expectedCmds)-1 {
|
||||
self.t.Errorf("ran too many commands. Unexpected command: `%s`", cmdObj.ToString())
|
||||
return "", errors.New("ran too many commands")
|
||||
}
|
||||
|
||||
expectedCmd := self.expectedCmds[self.expectedCmdIndex]
|
||||
output, err := expectedCmd(cmdObj)
|
||||
|
||||
self.expectedCmdIndex++
|
||||
|
||||
return output, err
|
||||
}
|
||||
|
||||
func (self *FakeCmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error {
|
||||
output, err := self.RunWithOutput(cmdObj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(strings.NewReader(output))
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
stop, err := onLine(line)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if stop {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *FakeCmdObjRunner) ExpectFunc(fn func(cmdObj ICmdObj) (string, error)) *FakeCmdObjRunner {
|
||||
self.expectedCmds = append(self.expectedCmds, fn)
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *FakeCmdObjRunner) Expect(expectedCmdStr string, output string, err error) *FakeCmdObjRunner {
|
||||
self.ExpectFunc(func(cmdObj ICmdObj) (string, error) {
|
||||
cmdStr := cmdObj.ToString()
|
||||
assert.Equal(self.t, expectedCmdStr, cmdStr, fmt.Sprintf("expected command %d to be %s, but was %s", self.expectedCmdIndex+1, expectedCmdStr, cmdStr))
|
||||
|
||||
return output, err
|
||||
})
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *FakeCmdObjRunner) ExpectArgs(expectedArgs []string, output string, err error) *FakeCmdObjRunner {
|
||||
self.ExpectFunc(func(cmdObj ICmdObj) (string, error) {
|
||||
args := cmdObj.GetCmd().Args
|
||||
assert.EqualValues(self.t, expectedArgs, args, fmt.Sprintf("command %d did not match expectation", self.expectedCmdIndex+1))
|
||||
|
||||
return output, err
|
||||
})
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
func (self *FakeCmdObjRunner) CheckForMissingCalls() {
|
||||
if self.expectedCmdIndex < len(self.expectedCmds) {
|
||||
self.t.Errorf("expected command %d to be called, but was not", self.expectedCmdIndex+1)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,45 +1,26 @@
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
"github.com/atotto/clipboard"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/mgutz/str"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Platform stores the os state
|
||||
type Platform struct {
|
||||
OS string
|
||||
CatCmd []string
|
||||
Shell string
|
||||
ShellArg string
|
||||
EscapedQuote string
|
||||
OpenCommand string
|
||||
OpenLinkCommand string
|
||||
}
|
||||
|
||||
// OSCommand holds all the os commands
|
||||
type OSCommand struct {
|
||||
Log *logrus.Entry
|
||||
Platform *Platform
|
||||
Config config.AppConfigurer
|
||||
Command func(string, ...string) *exec.Cmd
|
||||
BeforeExecuteCmd func(*exec.Cmd)
|
||||
Getenv func(string) string
|
||||
*common.Common
|
||||
Platform *Platform
|
||||
Getenv func(string) string
|
||||
|
||||
// callback to run before running a command, i.e. for the purposes of logging
|
||||
onRunCommand func(CmdLogEntry)
|
||||
@@ -48,6 +29,17 @@ type OSCommand struct {
|
||||
CmdLogSpan string
|
||||
|
||||
removeFile func(string) error
|
||||
|
||||
Cmd *CmdObjBuilder
|
||||
}
|
||||
|
||||
// Platform stores the os state
|
||||
type Platform struct {
|
||||
OS string
|
||||
Shell string
|
||||
ShellArg string
|
||||
OpenCommand string
|
||||
OpenLinkCommand string
|
||||
}
|
||||
|
||||
// TODO: make these fields private
|
||||
@@ -81,16 +73,20 @@ func NewCmdLogEntry(cmdStr string, span string, commandLine bool) CmdLogEntry {
|
||||
}
|
||||
|
||||
// NewOSCommand os command runner
|
||||
func NewOSCommand(log *logrus.Entry, config config.AppConfigurer) *OSCommand {
|
||||
return &OSCommand{
|
||||
Log: log,
|
||||
Platform: getPlatform(),
|
||||
Config: config,
|
||||
Command: secureexec.Command,
|
||||
BeforeExecuteCmd: func(*exec.Cmd) {},
|
||||
Getenv: os.Getenv,
|
||||
removeFile: os.RemoveAll,
|
||||
func NewOSCommand(common *common.Common) *OSCommand {
|
||||
platform := getPlatform()
|
||||
|
||||
c := &OSCommand{
|
||||
Common: common,
|
||||
Platform: platform,
|
||||
Getenv: os.Getenv,
|
||||
removeFile: os.RemoveAll,
|
||||
}
|
||||
|
||||
runner := &cmdObjRunner{log: common.Log, logCmdObj: c.LogCmdObj}
|
||||
c.Cmd = &CmdObjBuilder{runner: runner, logCmdObj: c.LogCmdObj, platform: platform}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *OSCommand) WithSpan(span string) *OSCommand {
|
||||
@@ -104,11 +100,13 @@ func (c *OSCommand) WithSpan(span string) *OSCommand {
|
||||
newOSCommand := &OSCommand{}
|
||||
*newOSCommand = *c
|
||||
newOSCommand.CmdLogSpan = span
|
||||
newOSCommand.Cmd.logCmdObj = newOSCommand.LogCmdObj
|
||||
newOSCommand.Cmd.runner = &cmdObjRunner{log: c.Log, logCmdObj: newOSCommand.LogCmdObj}
|
||||
return newOSCommand
|
||||
}
|
||||
|
||||
func (c *OSCommand) LogExecCmd(cmd *exec.Cmd) {
|
||||
c.LogCommand(strings.Join(cmd.Args, " "), true)
|
||||
func (c *OSCommand) LogCmdObj(cmdObj ICmdObj) {
|
||||
c.LogCommand(cmdObj.ToString(), true)
|
||||
}
|
||||
|
||||
func (c *OSCommand) LogCommand(cmdStr string, commandLine bool) {
|
||||
@@ -123,158 +121,13 @@ func (c *OSCommand) SetOnRunCommand(f func(CmdLogEntry)) {
|
||||
c.onRunCommand = f
|
||||
}
|
||||
|
||||
// SetCommand sets the command function used by the struct.
|
||||
// To be used for testing only
|
||||
func (c *OSCommand) SetCommand(cmd func(string, ...string) *exec.Cmd) {
|
||||
c.Command = cmd
|
||||
}
|
||||
|
||||
// To be used for testing only
|
||||
func (c *OSCommand) SetRemoveFile(f func(string) error) {
|
||||
c.removeFile = f
|
||||
}
|
||||
|
||||
func (c *OSCommand) SetBeforeExecuteCmd(cmd func(*exec.Cmd)) {
|
||||
c.BeforeExecuteCmd = cmd
|
||||
}
|
||||
|
||||
type RunCommandOptions struct {
|
||||
EnvVars []string
|
||||
}
|
||||
|
||||
func (c *OSCommand) RunCommandWithOutputWithOptions(command string, options RunCommandOptions) (string, error) {
|
||||
c.LogCommand(command, true)
|
||||
cmd := c.ExecutableFromString(command)
|
||||
|
||||
cmd.Env = append(cmd.Env, "GIT_TERMINAL_PROMPT=0") // prevents git from prompting us for input which would freeze the program
|
||||
cmd.Env = append(cmd.Env, options.EnvVars...)
|
||||
|
||||
return sanitisedCommandOutput(cmd.CombinedOutput())
|
||||
}
|
||||
|
||||
func (c *OSCommand) RunCommandWithOptions(command string, options RunCommandOptions) error {
|
||||
_, err := c.RunCommandWithOutputWithOptions(command, options)
|
||||
return err
|
||||
}
|
||||
|
||||
// RunCommandWithOutput wrapper around commands returning their output and error
|
||||
// NOTE: If you don't pass any formatArgs we'll just use the command directly,
|
||||
// however there's a bizarre compiler error/warning when you pass in a formatString
|
||||
// with a percent sign because it thinks it's supposed to be a formatString when
|
||||
// in that case it's not. To get around that error you'll need to define the string
|
||||
// in a variable and pass the variable into RunCommandWithOutput.
|
||||
func (c *OSCommand) RunCommandWithOutput(formatString string, formatArgs ...interface{}) (string, error) {
|
||||
command := formatString
|
||||
if formatArgs != nil {
|
||||
command = fmt.Sprintf(formatString, formatArgs...)
|
||||
}
|
||||
cmd := c.ExecutableFromString(command)
|
||||
c.LogExecCmd(cmd)
|
||||
output, err := sanitisedCommandOutput(cmd.CombinedOutput())
|
||||
if err != nil {
|
||||
c.Log.WithField("command", command).Error(output)
|
||||
}
|
||||
return output, err
|
||||
}
|
||||
|
||||
// RunExecutableWithOutput runs an executable file and returns its output
|
||||
func (c *OSCommand) RunExecutableWithOutput(cmd *exec.Cmd) (string, error) {
|
||||
c.LogExecCmd(cmd)
|
||||
c.BeforeExecuteCmd(cmd)
|
||||
return sanitisedCommandOutput(cmd.CombinedOutput())
|
||||
}
|
||||
|
||||
// RunExecutable runs an executable file and returns an error if there was one
|
||||
func (c *OSCommand) RunExecutable(cmd *exec.Cmd) error {
|
||||
_, err := c.RunExecutableWithOutput(cmd)
|
||||
return err
|
||||
}
|
||||
|
||||
// ExecutableFromString takes a string like `git status` and returns an executable command for it
|
||||
func (c *OSCommand) ExecutableFromString(commandStr string) *exec.Cmd {
|
||||
splitCmd := str.ToArgv(commandStr)
|
||||
cmd := c.Command(splitCmd[0], splitCmd[1:]...)
|
||||
cmd.Env = append(os.Environ(), "GIT_OPTIONAL_LOCKS=0")
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ShellCommandFromString takes a string like `git commit` and returns an executable shell command for it
|
||||
func (c *OSCommand) ShellCommandFromString(commandStr string) *exec.Cmd {
|
||||
quotedCommand := ""
|
||||
// Windows does not seem to like quotes around the command
|
||||
if c.Platform.OS == "windows" {
|
||||
quotedCommand = commandStr
|
||||
} else {
|
||||
quotedCommand = c.Quote(commandStr)
|
||||
}
|
||||
|
||||
shellCommand := fmt.Sprintf("%s %s %s", c.Platform.Shell, c.Platform.ShellArg, quotedCommand)
|
||||
return c.ExecutableFromString(shellCommand)
|
||||
}
|
||||
|
||||
// RunCommandWithOutputLive runs RunCommandWithOutputLiveWrapper
|
||||
func (c *OSCommand) RunCommandWithOutputLive(command string, output func(string) string) error {
|
||||
return RunCommandWithOutputLiveWrapper(c, command, output)
|
||||
}
|
||||
|
||||
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...)
|
||||
return err
|
||||
}
|
||||
|
||||
// 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)
|
||||
c.LogExecCmd(cmd)
|
||||
|
||||
_, err := sanitisedCommandOutput(cmd.CombinedOutput())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// FileType tells us if the file is a file, directory or other
|
||||
func (c *OSCommand) FileType(path string) string {
|
||||
func FileType(path string) string {
|
||||
fileInfo, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return "other"
|
||||
@@ -285,73 +138,51 @@ func (c *OSCommand) FileType(path string) string {
|
||||
return "file"
|
||||
}
|
||||
|
||||
func sanitisedCommandOutput(output []byte, err error) (string, error) {
|
||||
outputString := string(output)
|
||||
if err != nil {
|
||||
// errors like 'exit status 1' are not very useful so we'll create an error
|
||||
// from the combined output
|
||||
if outputString == "" {
|
||||
return "", utils.WrapError(err)
|
||||
}
|
||||
return outputString, errors.New(outputString)
|
||||
}
|
||||
return outputString, nil
|
||||
}
|
||||
|
||||
// OpenFile opens a file with the given
|
||||
func (c *OSCommand) OpenFile(filename string) error {
|
||||
commandTemplate := c.Config.GetUserConfig().OS.OpenCommand
|
||||
commandTemplate := c.UserConfig.OS.OpenCommand
|
||||
templateValues := map[string]string{
|
||||
"filename": c.Quote(filename),
|
||||
}
|
||||
|
||||
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
|
||||
err := c.RunCommand(command)
|
||||
return err
|
||||
return c.Cmd.NewShell(command).Run()
|
||||
}
|
||||
|
||||
// OpenLink opens a file with the given
|
||||
func (c *OSCommand) OpenLink(link string) error {
|
||||
c.LogCommand(fmt.Sprintf("Opening link '%s'", link), false)
|
||||
commandTemplate := c.Config.GetUserConfig().OS.OpenLinkCommand
|
||||
commandTemplate := c.UserConfig.OS.OpenLinkCommand
|
||||
templateValues := map[string]string{
|
||||
"link": c.Quote(link),
|
||||
}
|
||||
|
||||
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
|
||||
err := c.RunCommand(command)
|
||||
return err
|
||||
}
|
||||
|
||||
// PrepareSubProcess iniPrepareSubProcessrocess then tells the Gui to switch to it
|
||||
// TODO: see if this needs to exist, given that ExecutableFromString does the same things
|
||||
func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *exec.Cmd {
|
||||
cmd := c.Command(cmdName, commandArgs...)
|
||||
if cmd != nil {
|
||||
cmd.Env = append(os.Environ(), "GIT_OPTIONAL_LOCKS=0")
|
||||
}
|
||||
c.LogExecCmd(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// PrepareShellSubProcess returns the pointer to a custom command
|
||||
func (c *OSCommand) PrepareShellSubProcess(command string) *exec.Cmd {
|
||||
return c.PrepareSubProcess(c.Platform.Shell, c.Platform.ShellArg, command)
|
||||
return c.Cmd.NewShell(command).Run()
|
||||
}
|
||||
|
||||
// Quote wraps a message in platform-specific quotation marks
|
||||
func (c *OSCommand) Quote(message string) string {
|
||||
if c.Platform.OS == "windows" {
|
||||
message = strings.Replace(message, `"`, `"'"'"`, -1)
|
||||
message = strings.Replace(message, `\"`, `\\"`, -1)
|
||||
return c.Cmd.Quote(message)
|
||||
}
|
||||
|
||||
func (self *CmdObjBuilder) Quote(message string) string {
|
||||
var quote string
|
||||
if self.platform.OS == "windows" {
|
||||
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
|
||||
@@ -425,24 +256,6 @@ func (c *OSCommand) FileExists(path string) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// RunPreparedCommand takes a pointer to an exec.Cmd and runs it
|
||||
// this is useful if you need to give your command some environment variables
|
||||
// before running it
|
||||
func (c *OSCommand) RunPreparedCommand(cmd *exec.Cmd) error {
|
||||
c.BeforeExecuteCmd(cmd)
|
||||
c.LogExecCmd(cmd)
|
||||
out, err := cmd.CombinedOutput()
|
||||
outString := string(out)
|
||||
c.Log.Info(outString)
|
||||
if err != nil {
|
||||
if len(outString) == 0 {
|
||||
return err
|
||||
}
|
||||
return errors.New(outString)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLazygitPath returns the path of the currently executed file
|
||||
func (c *OSCommand) GetLazygitPath() string {
|
||||
ex, err := os.Executable() // get the executable path for git to use
|
||||
@@ -461,7 +274,7 @@ func (c *OSCommand) PipeCommands(commandStrings ...string) error {
|
||||
logCmdStr += " | "
|
||||
}
|
||||
logCmdStr += str
|
||||
cmds[i] = c.ExecutableFromString(str)
|
||||
cmds[i] = c.Cmd.New(str).GetCmd()
|
||||
}
|
||||
c.LogCommand(logCmdStr, true)
|
||||
|
||||
@@ -524,37 +337,10 @@ func Kill(cmd *exec.Cmd) error {
|
||||
return cmd.Process.Kill()
|
||||
}
|
||||
|
||||
func RunLineOutputCmd(cmd *exec.Cmd, onLine func(line string) (bool, error)) error {
|
||||
stdoutPipe, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(stdoutPipe)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
stop, err := onLine(line)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if stop {
|
||||
_ = cmd.Process.Kill()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
_ = cmd.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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 +349,7 @@ func (c *OSCommand) RemoveFile(path string) error {
|
||||
|
||||
return c.removeFile(path)
|
||||
}
|
||||
|
||||
func GetTempDir() string {
|
||||
return filepath.Join(os.TempDir(), "lazygit")
|
||||
}
|
||||
|
||||
@@ -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}}",
|
||||
}
|
||||
|
||||
182
pkg/commands/oscommands/os_default_test.go
Normal file
182
pkg/commands/oscommands/os_default_test.go
Normal file
@@ -0,0 +1,182 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOSCommandOpenFileDarwin(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
runner *FakeCmdObjRunner
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
filename: "test",
|
||||
runner: NewFakeRunner(t).
|
||||
ExpectArgs([]string{"bash", "-c", `open "test"`}, "", errors.New("error")),
|
||||
test: func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
filename: "test",
|
||||
runner: NewFakeRunner(t).
|
||||
ExpectArgs([]string{"bash", "-c", `open "test"`}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
filename: "filename with spaces",
|
||||
runner: NewFakeRunner(t).
|
||||
ExpectArgs([]string{"bash", "-c", `open "filename with spaces"`}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
oSCmd := NewDummyOSCommandWithRunner(s.runner)
|
||||
oSCmd.Platform.OS = "darwin"
|
||||
oSCmd.UserConfig.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
|
||||
runner *FakeCmdObjRunner
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
filename: "test",
|
||||
runner: NewFakeRunner(t).
|
||||
ExpectArgs([]string{"bash", "-c", `xdg-open "test" > /dev/null`}, "", errors.New("error")),
|
||||
test: func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
filename: "test",
|
||||
runner: NewFakeRunner(t).
|
||||
ExpectArgs([]string{"bash", "-c", `xdg-open "test" > /dev/null`}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
filename: "filename with spaces",
|
||||
runner: NewFakeRunner(t).
|
||||
ExpectArgs([]string{"bash", "-c", `xdg-open "filename with spaces" > /dev/null`}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
filename: "let's_test_with_single_quote",
|
||||
runner: NewFakeRunner(t).
|
||||
ExpectArgs([]string{"bash", "-c", `xdg-open "let's_test_with_single_quote" > /dev/null`}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
filename: "$USER.txt",
|
||||
runner: NewFakeRunner(t).
|
||||
ExpectArgs([]string{"bash", "-c", `xdg-open "\$USER.txt" > /dev/null`}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
oSCmd := NewDummyOSCommandWithRunner(s.runner)
|
||||
oSCmd.Platform.OS = "linux"
|
||||
oSCmd.UserConfig.OS.OpenCommand = `xdg-open {{filename}} > /dev/null`
|
||||
|
||||
s.test(oSCmd.OpenFile(s.filename))
|
||||
}
|
||||
}
|
||||
|
||||
func TestOSCommandOpenFileWindows(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
runner *FakeCmdObjRunner
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
filename: "test",
|
||||
runner: NewFakeRunner(t).
|
||||
ExpectArgs([]string{"cmd", "/c", "start", "", "test"}, "", errors.New("error")),
|
||||
test: func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
filename: "test",
|
||||
runner: NewFakeRunner(t).
|
||||
ExpectArgs([]string{"cmd", "/c", "start", "", "test"}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
filename: "filename with spaces",
|
||||
runner: NewFakeRunner(t).
|
||||
ExpectArgs([]string{"cmd", "/c", "start", "", "filename with spaces"}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
filename: "let's_test_with_single_quote",
|
||||
runner: NewFakeRunner(t).
|
||||
ExpectArgs([]string{"cmd", "/c", "start", "", "let's_test_with_single_quote"}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
filename: "$USER.txt",
|
||||
runner: NewFakeRunner(t).
|
||||
ExpectArgs([]string{"cmd", "/c", "start", "", "$USER.txt"}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
oSCmd := NewDummyOSCommandWithRunner(s.runner)
|
||||
platform := &Platform{
|
||||
OS: "windows",
|
||||
Shell: "cmd",
|
||||
ShellArg: "/c",
|
||||
}
|
||||
oSCmd.Platform = platform
|
||||
oSCmd.Cmd.platform = platform
|
||||
oSCmd.UserConfig.OS.OpenCommand = `start "" {{filename}}`
|
||||
|
||||
s.test(oSCmd.OpenFile(s.filename))
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,12 @@ package oscommands
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestOSCommandRunCommandWithOutput is a function.
|
||||
func TestOSCommandRunCommandWithOutput(t *testing.T) {
|
||||
func TestOSCommandRunWithOutput(t *testing.T) {
|
||||
type scenario struct {
|
||||
command string
|
||||
test func(string, error)
|
||||
@@ -34,12 +31,12 @@ func TestOSCommandRunCommandWithOutput(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s.test(NewDummyOSCommand().RunCommandWithOutput(s.command))
|
||||
c := NewDummyOSCommand()
|
||||
s.test(c.Cmd.New(s.command).RunWithOutput())
|
||||
}
|
||||
}
|
||||
|
||||
// TestOSCommandRunCommand is a function.
|
||||
func TestOSCommandRunCommand(t *testing.T) {
|
||||
func TestOSCommandRun(t *testing.T) {
|
||||
type scenario struct {
|
||||
command string
|
||||
test func(error)
|
||||
@@ -55,62 +52,11 @@ func TestOSCommandRunCommand(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s.test(NewDummyOSCommand().RunCommand(s.command))
|
||||
c := NewDummyOSCommand()
|
||||
s.test(c.Cmd.New(s.command).Run())
|
||||
}
|
||||
}
|
||||
|
||||
// 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 +64,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 +77,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 +90,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,14 +101,13 @@ 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)
|
||||
}
|
||||
|
||||
// TestOSCommandFileType is a function.
|
||||
func TestOSCommandFileType(t *testing.T) {
|
||||
type scenario struct {
|
||||
path string
|
||||
@@ -215,7 +160,7 @@ func TestOSCommandFileType(t *testing.T) {
|
||||
|
||||
for _, s := range scenarios {
|
||||
s.setup()
|
||||
s.test(NewDummyOSCommand().FileType(s.path))
|
||||
s.test(FileType(s.path))
|
||||
_ = os.RemoveAll(s.path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
78
pkg/commands/oscommands/os_windows_test.go
Normal file
78
pkg/commands/oscommands/os_windows_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOSCommandOpenFileWindows(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
runner *FakeCmdObjRunner
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
filename: "test",
|
||||
runner: NewFakeRunner(t).
|
||||
ExpectArgs([]string{"cmd", "/c", "start", "", "test"}, "", errors.New("error")),
|
||||
test: func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
filename: "test",
|
||||
runner: NewFakeRunner(t).
|
||||
ExpectArgs([]string{"cmd", "/c", "start", "", "test"}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
filename: "filename with spaces",
|
||||
runner: NewFakeRunner(t).
|
||||
ExpectArgs([]string{"cmd", "/c", "start", "", "filename with spaces"}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
filename: "let's_test_with_single_quote",
|
||||
runner: NewFakeRunner(t).
|
||||
ExpectArgs([]string{"cmd", "/c", "start", "", "let's_test_with_single_quote"}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
filename: "$USER.txt",
|
||||
runner: NewFakeRunner(t).
|
||||
ExpectArgs([]string{"cmd", "/c", "start", "", "$USER.txt"}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
oSCmd := NewDummyOSCommandWithRunner(s.runner)
|
||||
platform := &Platform{
|
||||
OS: "windows",
|
||||
Shell: "cmd",
|
||||
ShellArg: "/c",
|
||||
}
|
||||
oSCmd.Platform = platform
|
||||
oSCmd.Cmd.platform = platform
|
||||
oSCmd.UserConfig.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 {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
||||
)
|
||||
|
||||
// DeletePatchesFromCommit applies a patch in reverse for a commit
|
||||
@@ -85,12 +86,12 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
|
||||
todo = a + " " + commit.Sha + " " + commit.Name + "\n" + todo
|
||||
}
|
||||
|
||||
cmd, err := c.PrepareInteractiveRebaseCommand(commits[baseIndex].Sha, todo, true)
|
||||
cmdObj, err := c.PrepareInteractiveRebaseCommand(commits[baseIndex].Sha, todo, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.OSCommand.RunPreparedCommand(cmd); err != nil {
|
||||
if err := cmdObj.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -149,7 +150,7 @@ func (c *GitCommand) MovePatchIntoIndex(commits []*models.Commit, commitIdx int,
|
||||
}
|
||||
|
||||
if err := p.ApplyPatches(true); err != nil {
|
||||
if c.WorkingTreeState() == REBASE_MODE_REBASING {
|
||||
if c.WorkingTreeState() == enums.REBASE_MODE_REBASING {
|
||||
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -169,7 +170,7 @@ func (c *GitCommand) MovePatchIntoIndex(commits []*models.Commit, commitIdx int,
|
||||
c.onSuccessfulContinue = func() error {
|
||||
// add patches to index
|
||||
if err := p.ApplyPatches(false); err != nil {
|
||||
if c.WorkingTreeState() == REBASE_MODE_REBASING {
|
||||
if c.WorkingTreeState() == enums.REBASE_MODE_REBASING {
|
||||
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -217,7 +218,7 @@ func (c *GitCommand) PullPatchIntoNewCommit(commits []*models.Commit, commitIdx
|
||||
|
||||
head_message, _ := c.GetHeadCommitMessage()
|
||||
new_message := fmt.Sprintf("Split from \"%s\"", head_message)
|
||||
err := c.OSCommand.RunCommand(c.CommitCmdStr(new_message, ""))
|
||||
err := c.CommitCmdObj(new_message, "").Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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
|
||||
}
|
||||
|
||||
// PullRequest opens a link in browser to create new pull request
|
||||
// with selected branch
|
||||
type PullRequest struct {
|
||||
GitServices []*Service
|
||||
GitCommand *GitCommand
|
||||
}
|
||||
|
||||
// RepoInformation holds some basic information about the repo
|
||||
type RepoInformation struct {
|
||||
Owner string
|
||||
Repository string
|
||||
}
|
||||
|
||||
// NewService builds a Service based on the host type
|
||||
func NewService(typeName string, repositoryDomain string, siteDomain string) *Service {
|
||||
var service *Service
|
||||
|
||||
switch typeName {
|
||||
case "github":
|
||||
service = &Service{
|
||||
Name: repositoryDomain,
|
||||
PullRequestURL: fmt.Sprintf("https://%s%s", siteDomain, "/%s/%s/compare/%s?expand=1"),
|
||||
}
|
||||
case "bitbucket":
|
||||
service = &Service{
|
||||
Name: repositoryDomain,
|
||||
PullRequestURL: fmt.Sprintf("https://%s%s", siteDomain, "/%s/%s/pull-requests/new?source=%s&t=1"),
|
||||
}
|
||||
case "gitlab":
|
||||
service = &Service{
|
||||
Name: repositoryDomain,
|
||||
PullRequestURL: fmt.Sprintf("https://%s%s", siteDomain, "/%s/%s/merge_requests/new?merge_request[source_branch]=%s"),
|
||||
}
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
func getServices(config config.AppConfigurer) []*Service {
|
||||
services := []*Service{
|
||||
NewService("github", "github.com", "github.com"),
|
||||
NewService("bitbucket", "bitbucket.org", "bitbucket.org"),
|
||||
NewService("gitlab", "gitlab.com", "gitlab.com"),
|
||||
}
|
||||
|
||||
configServices := config.GetUserConfig().Services
|
||||
|
||||
for repoDomain, typeAndDomain := range configServices {
|
||||
splitData := strings.Split(typeAndDomain, ":")
|
||||
if len(splitData) != 2 {
|
||||
// TODO log this misconfiguration
|
||||
continue
|
||||
}
|
||||
|
||||
service := NewService(splitData[0], repoDomain, splitData[1])
|
||||
if service == nil {
|
||||
// TODO log this unsupported service
|
||||
continue
|
||||
}
|
||||
|
||||
services = append(services, service)
|
||||
}
|
||||
|
||||
return services
|
||||
}
|
||||
|
||||
// NewPullRequest creates new instance of PullRequest
|
||||
func NewPullRequest(gitCommand *GitCommand) *PullRequest {
|
||||
return &PullRequest{
|
||||
GitServices: getServices(gitCommand.Config),
|
||||
GitCommand: gitCommand,
|
||||
}
|
||||
}
|
||||
|
||||
// Create opens link to new pull request in browser
|
||||
func (pr *PullRequest) Create(branch *models.Branch) (string, error) {
|
||||
pullRequestURL, err := pr.getPullRequestURL(branch)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return pullRequestURL, pr.GitCommand.OSCommand.OpenLink(pullRequestURL)
|
||||
}
|
||||
|
||||
// CopyURL copies the pull request URL to the clipboard
|
||||
func (pr *PullRequest) CopyURL(branch *models.Branch) (string, error) {
|
||||
pullRequestURL, err := pr.getPullRequestURL(branch)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return pullRequestURL, pr.GitCommand.OSCommand.CopyToClipboard(pullRequestURL)
|
||||
}
|
||||
|
||||
func (pr *PullRequest) getPullRequestURL(branch *models.Branch) (string, error) {
|
||||
branchExistsOnRemote := pr.GitCommand.CheckRemoteBranchExists(branch)
|
||||
|
||||
if !branchExistsOnRemote {
|
||||
return "", errors.New(pr.GitCommand.Tr.NoBranchOnRemote)
|
||||
}
|
||||
|
||||
repoURL := pr.GitCommand.GetRemoteURL()
|
||||
var gitService *Service
|
||||
|
||||
for _, service := range pr.GitServices {
|
||||
if strings.Contains(repoURL, service.Name) {
|
||||
gitService = service
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if gitService == nil {
|
||||
return "", errors.New(pr.GitCommand.Tr.UnsupportedGitService)
|
||||
}
|
||||
|
||||
repoInfo := getRepoInfoFromURL(repoURL)
|
||||
pullRequestURL := fmt.Sprintf(
|
||||
gitService.PullRequestURL, repoInfo.Owner, repoInfo.Repository, branch.Name,
|
||||
)
|
||||
|
||||
return pullRequestURL, nil
|
||||
}
|
||||
|
||||
func getRepoInfoFromURL(url string) *RepoInformation {
|
||||
isHTTP := strings.HasPrefix(url, "http")
|
||||
|
||||
if isHTTP {
|
||||
splits := strings.Split(url, "/")
|
||||
owner := strings.Join(splits[3:len(splits)-1], "/")
|
||||
repo := strings.TrimSuffix(splits[len(splits)-1], ".git")
|
||||
|
||||
return &RepoInformation{
|
||||
Owner: owner,
|
||||
Repository: repo,
|
||||
}
|
||||
}
|
||||
|
||||
tmpSplit := strings.Split(url, ":")
|
||||
splits := strings.Split(tmpSplit[1], "/")
|
||||
owner := strings.Join(splits[0:len(splits)-1], "/")
|
||||
repo := strings.TrimSuffix(splits[len(splits)-1], ".git")
|
||||
|
||||
return &RepoInformation{
|
||||
Owner: owner,
|
||||
Repository: repo,
|
||||
}
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// TestGetRepoInfoFromURL is a function.
|
||||
func TestGetRepoInfoFromURL(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
repoURL string
|
||||
test func(*RepoInformation)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Returns repository information for git remote url",
|
||||
"git@github.com:petersmith/super_calculator",
|
||||
func(repoInfo *RepoInformation) {
|
||||
assert.EqualValues(t, repoInfo.Owner, "petersmith")
|
||||
assert.EqualValues(t, repoInfo.Repository, "super_calculator")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Returns repository information for http remote url",
|
||||
"https://my_username@bitbucket.org/johndoe/social_network.git",
|
||||
func(repoInfo *RepoInformation) {
|
||||
assert.EqualValues(t, repoInfo.Owner, "johndoe")
|
||||
assert.EqualValues(t, repoInfo.Repository, "social_network")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
s.test(getRepoInfoFromURL(s.repoURL))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreatePullRequest is a function.
|
||||
func TestCreatePullRequest(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
branch *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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,15 @@ package commands
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/mgutz/str"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
)
|
||||
|
||||
func (c *GitCommand) RewordCommit(commits []*models.Commit, index int) (*exec.Cmd, error) {
|
||||
func (c *GitCommand) RewordCommit(commits []*models.Commit, index int) (oscommands.ICmdObj, error) {
|
||||
todo, sha, err := c.GenerateGenericRebaseTodo(commits, index, "reword")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -35,12 +33,12 @@ func (c *GitCommand) MoveCommitDown(commits []*models.Commit, index int) error {
|
||||
todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo
|
||||
}
|
||||
|
||||
cmd, err := c.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todo, true)
|
||||
cmdObj, err := c.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todo, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.OSCommand.RunPreparedCommand(cmd)
|
||||
return cmdObj.Run()
|
||||
}
|
||||
|
||||
func (c *GitCommand) InteractiveRebase(commits []*models.Commit, index int, action string) error {
|
||||
@@ -49,30 +47,29 @@ func (c *GitCommand) InteractiveRebase(commits []*models.Commit, index int, acti
|
||||
return err
|
||||
}
|
||||
|
||||
cmd, err := c.PrepareInteractiveRebaseCommand(sha, todo, true)
|
||||
cmdObj, err := c.PrepareInteractiveRebaseCommand(sha, todo, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.OSCommand.RunPreparedCommand(cmd)
|
||||
return cmdObj.Run()
|
||||
}
|
||||
|
||||
// PrepareInteractiveRebaseCommand returns the cmd for an interactive rebase
|
||||
// we tell git to run lazygit to edit the todo list, and we pass the client
|
||||
// lazygit a todo string to write to the todo file
|
||||
func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string, overrideEditor bool) (*exec.Cmd, error) {
|
||||
func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string, overrideEditor bool) (oscommands.ICmdObj, error) {
|
||||
ex := c.OSCommand.GetLazygitPath()
|
||||
|
||||
debug := "FALSE"
|
||||
if c.OSCommand.Config.GetDebug() {
|
||||
if c.Debug {
|
||||
debug = "TRUE"
|
||||
}
|
||||
|
||||
cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty %s", baseSha)
|
||||
c.Log.WithField("command", cmdStr).Info("RunCommand")
|
||||
splitCmd := str.ToArgv(cmdStr)
|
||||
|
||||
cmd := c.OSCommand.Command(splitCmd[0], splitCmd[1:]...)
|
||||
cmdObj := c.Cmd.New(cmdStr)
|
||||
|
||||
gitSequenceEditor := ex
|
||||
if todo == "" {
|
||||
@@ -81,9 +78,7 @@ func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string
|
||||
c.OSCommand.LogCommand(fmt.Sprintf("Creating TODO file for interactive rebase: \n\n%s", todo), false)
|
||||
}
|
||||
|
||||
cmd.Env = os.Environ()
|
||||
cmd.Env = append(
|
||||
cmd.Env,
|
||||
cmdObj.AddEnvVars(
|
||||
"LAZYGIT_CLIENT_COMMAND=INTERACTIVE_REBASE",
|
||||
"LAZYGIT_REBASE_TODO="+todo,
|
||||
"DEBUG="+debug,
|
||||
@@ -93,10 +88,10 @@ func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string
|
||||
)
|
||||
|
||||
if overrideEditor {
|
||||
cmd.Env = append(cmd.Env, "GIT_EDITOR="+ex)
|
||||
cmdObj.AddEnvVars("GIT_EDITOR=" + ex)
|
||||
}
|
||||
|
||||
return cmd, nil
|
||||
return cmdObj, nil
|
||||
}
|
||||
|
||||
func (c *GitCommand) GenerateGenericRebaseTodo(commits []*models.Commit, actionIndex int, action string) (string, string, error) {
|
||||
@@ -119,7 +114,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.
|
||||
@@ -222,26 +217,22 @@ func (c *GitCommand) BeginInteractiveRebaseForCommit(commits []*models.Commit, c
|
||||
return err
|
||||
}
|
||||
|
||||
cmd, err := c.PrepareInteractiveRebaseCommand(sha, todo, true)
|
||||
cmdObj, err := c.PrepareInteractiveRebaseCommand(sha, todo, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.OSCommand.RunPreparedCommand(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return cmdObj.Run()
|
||||
}
|
||||
|
||||
// RebaseBranch interactive rebases onto a branch
|
||||
func (c *GitCommand) RebaseBranch(branchName string) error {
|
||||
cmd, err := c.PrepareInteractiveRebaseCommand(branchName, "", false)
|
||||
cmdObj, err := c.PrepareInteractiveRebaseCommand(branchName, "", false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.OSCommand.RunPreparedCommand(cmd)
|
||||
return cmdObj.Run()
|
||||
}
|
||||
|
||||
// GenericMerge takes a commandType of "merge" or "rebase" and a command of "abort", "skip" or "continue"
|
||||
@@ -276,14 +267,14 @@ func (c *GitCommand) GenericMergeOrRebaseAction(commandType string, command stri
|
||||
}
|
||||
|
||||
func (c *GitCommand) runSkipEditorCommand(command string) error {
|
||||
cmd := c.OSCommand.ExecutableFromString(command)
|
||||
cmdObj := c.OSCommand.Cmd.New(command)
|
||||
lazyGitPath := c.OSCommand.GetLazygitPath()
|
||||
cmd.Env = append(
|
||||
cmd.Env,
|
||||
"LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY",
|
||||
"GIT_EDITOR="+lazyGitPath,
|
||||
"EDITOR="+lazyGitPath,
|
||||
"VISUAL="+lazyGitPath,
|
||||
)
|
||||
return c.OSCommand.RunExecutable(cmd)
|
||||
return cmdObj.
|
||||
AddEnvVars(
|
||||
"LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY",
|
||||
"GIT_EDITOR="+lazyGitPath,
|
||||
"EDITOR="+lazyGitPath,
|
||||
"VISUAL="+lazyGitPath,
|
||||
).
|
||||
Run()
|
||||
}
|
||||
|
||||
@@ -1,57 +1,47 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/test"
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandRebaseBranch is a function.
|
||||
func TestGitCommandRebaseBranch(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
arg string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
runner *oscommands.FakeCmdObjRunner
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"successful rebase",
|
||||
"master",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git rebase --interactive --autostash --keep-empty master",
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
testName: "successful rebase",
|
||||
arg: "master",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git rebase --interactive --autostash --keep-empty master`, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"unsuccessful rebase",
|
||||
"master",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git rebase --interactive --autostash --keep-empty master",
|
||||
Replace: "test",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
testName: "unsuccessful rebase",
|
||||
arg: "master",
|
||||
runner: oscommands.NewFakeRunner(t).
|
||||
Expect(`git rebase --interactive --autostash --keep-empty master`, "", errors.New("error")),
|
||||
test: func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
gitCmd := NewDummyGitCommandWithRunner(s.runner)
|
||||
s.test(gitCmd.RebaseBranch(s.arg))
|
||||
})
|
||||
}
|
||||
@@ -60,37 +50,27 @@ func TestGitCommandRebaseBranch(t *testing.T) {
|
||||
// TestGitCommandSkipEditorCommand confirms that SkipEditorCommand injects
|
||||
// environment variables that suppress an interactive editor
|
||||
func TestGitCommandSkipEditorCommand(t *testing.T) {
|
||||
cmd := NewDummyGitCommand()
|
||||
|
||||
cmd.OSCommand.SetBeforeExecuteCmd(func(cmd *exec.Cmd) {
|
||||
test.AssertContainsMatch(
|
||||
t,
|
||||
cmd.Env,
|
||||
regexp.MustCompile("^VISUAL="),
|
||||
"expected VISUAL to be set for a non-interactive external command",
|
||||
)
|
||||
|
||||
test.AssertContainsMatch(
|
||||
t,
|
||||
cmd.Env,
|
||||
regexp.MustCompile("^EDITOR="),
|
||||
"expected EDITOR to be set for a non-interactive external command",
|
||||
)
|
||||
|
||||
test.AssertContainsMatch(
|
||||
t,
|
||||
cmd.Env,
|
||||
regexp.MustCompile("^GIT_EDITOR="),
|
||||
"expected GIT_EDITOR to be set for a non-interactive external command",
|
||||
)
|
||||
|
||||
test.AssertContainsMatch(
|
||||
t,
|
||||
cmd.Env,
|
||||
regexp.MustCompile("^LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY$"),
|
||||
"expected LAZYGIT_CLIENT_COMMAND to be set for a non-interactive external command",
|
||||
)
|
||||
commandStr := "git blah"
|
||||
runner := oscommands.NewFakeRunner(t).ExpectFunc(func(cmdObj oscommands.ICmdObj) (string, error) {
|
||||
assert.Equal(t, commandStr, cmdObj.ToString())
|
||||
envVars := cmdObj.GetEnvVars()
|
||||
for _, regexStr := range []string{
|
||||
`^VISUAL=.*$`,
|
||||
`^EDITOR=.*$`,
|
||||
`^GIT_EDITOR=.*$`,
|
||||
"^LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY$",
|
||||
} {
|
||||
foundMatch := utils.IncludesStringFunc(envVars, func(envVar string) bool {
|
||||
return regexp.MustCompile(regexStr).MatchString(envVar)
|
||||
})
|
||||
if !foundMatch {
|
||||
t.Errorf("expected environment variable %s to be set", regexStr)
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
})
|
||||
|
||||
_ = cmd.runSkipEditorCommand("true")
|
||||
gitCmd := NewDummyGitCommandWithRunner(runner)
|
||||
err := gitCmd.runSkipEditorCommand(commandStr)
|
||||
assert.NoError(t, err)
|
||||
runner.CheckForMissingCalls()
|
||||
}
|
||||
|
||||
@@ -3,41 +3,57 @@ 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.Cmd.
|
||||
New(fmt.Sprintf("git remote add %s %s", c.Cmd.Quote(name), c.Cmd.Quote(url))).
|
||||
Run()
|
||||
}
|
||||
|
||||
func (c *GitCommand) RemoveRemote(name string) error {
|
||||
return c.RunCommand("git remote remove %s", name)
|
||||
return c.Cmd.
|
||||
New(fmt.Sprintf("git remote remove %s", c.Cmd.Quote(name))).
|
||||
Run()
|
||||
}
|
||||
|
||||
func (c *GitCommand) RenameRemote(oldRemoteName string, newRemoteName string) error {
|
||||
return c.RunCommand("git remote rename %s %s", oldRemoteName, newRemoteName)
|
||||
return c.Cmd.
|
||||
New(fmt.Sprintf("git remote rename %s %s", c.Cmd.Quote(oldRemoteName), c.Cmd.Quote(newRemoteName))).
|
||||
Run()
|
||||
}
|
||||
|
||||
func (c *GitCommand) UpdateRemoteUrl(remoteName string, updatedUrl string) error {
|
||||
return c.RunCommand("git remote set-url %s %s", remoteName, updatedUrl)
|
||||
return c.Cmd.
|
||||
New(fmt.Sprintf("git remote set-url %s %s", c.Cmd.Quote(remoteName), c.Cmd.Quote(updatedUrl))).
|
||||
Run()
|
||||
}
|
||||
|
||||
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.Cmd.Quote(remoteName), c.Cmd.Quote(branchName))
|
||||
cmdObj := c.Cmd.
|
||||
New(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 {
|
||||
_, err := c.OSCommand.RunCommandWithOutput(
|
||||
"git show-ref --verify -- refs/remotes/origin/%s",
|
||||
branch.Name,
|
||||
)
|
||||
func (c *GitCommand) CheckRemoteBranchExists(branchName string) bool {
|
||||
_, err := c.Cmd.
|
||||
New(
|
||||
fmt.Sprintf("git show-ref --verify -- refs/remotes/origin/%s",
|
||||
c.Cmd.Quote(branchName),
|
||||
)).
|
||||
RunWithOutput()
|
||||
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// GetRemoteURL returns current repo remote url
|
||||
func (c *GitCommand) GetRemoteURL() string {
|
||||
return c.GetConfigValue("remote.origin.url")
|
||||
return c.GitConfig.Get("remote.origin.url")
|
||||
}
|
||||
|
||||
@@ -1,28 +1,32 @@
|
||||
package commands
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
|
||||
)
|
||||
|
||||
// StashDo modify stash
|
||||
func (c *GitCommand) StashDo(index int, method string) error {
|
||||
return c.RunCommand("git stash %s stash@{%d}", method, index)
|
||||
return c.Cmd.New(fmt.Sprintf("git stash %s stash@{%d}", method, index)).Run()
|
||||
}
|
||||
|
||||
// StashSave save stash
|
||||
// TODO: before calling this, check if there is anything to save
|
||||
func (c *GitCommand) StashSave(message string) error {
|
||||
return c.RunCommand("git stash save %s", c.OSCommand.Quote(message))
|
||||
return c.Cmd.New("git stash save " + c.OSCommand.Quote(message)).Run()
|
||||
}
|
||||
|
||||
// GetStashEntryDiff stash diff
|
||||
func (c *GitCommand) ShowStashEntryCmdStr(index int) string {
|
||||
return fmt.Sprintf("git stash show -p --stat --color=%s stash@{%d}", c.colorArg(), index)
|
||||
return fmt.Sprintf("git stash show -p --stat --color=%s --unified=%d stash@{%d}", c.colorArg(), c.UserConfig.Git.DiffContextSize, index)
|
||||
}
|
||||
|
||||
// StashSaveStagedChanges stashes only the currently staged changes. This takes a few steps
|
||||
// shoutouts to Joe on https://stackoverflow.com/questions/14759748/stashing-only-staged-changes-in-git-is-it-possible
|
||||
func (c *GitCommand) StashSaveStagedChanges(message string) error {
|
||||
// wrap in 'writing', which uses a mutex
|
||||
if err := c.RunCommand("git stash --keep-index"); err != nil {
|
||||
if err := c.Cmd.New("git stash --keep-index").Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -30,7 +34,7 @@ func (c *GitCommand) StashSaveStagedChanges(message string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.RunCommand("git stash apply stash@{1}"); err != nil {
|
||||
if err := c.Cmd.New("git stash apply stash@{1}").Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -38,14 +42,17 @@ func (c *GitCommand) StashSaveStagedChanges(message string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.RunCommand("git stash drop stash@{1}"); err != nil {
|
||||
if err := c.Cmd.New("git stash drop stash@{1}").Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if you had staged an untracked file, that will now appear as 'AD' in git status
|
||||
// meaning it's deleted in your working tree but added in your index. Given that it's
|
||||
// now safely stashed, we need to remove it.
|
||||
files := c.GetStatusFiles(GetStatusFileOptions{})
|
||||
files := loaders.
|
||||
NewFileLoader(c.Common, c.Cmd, c.GitConfig).
|
||||
GetStatusFiles(loaders.GetStatusFileOptions{})
|
||||
|
||||
for _, file := range files {
|
||||
if file.ShortStatus == "AD" {
|
||||
if err := c.UnStageFile(file.Names(), false); err != nil {
|
||||
|
||||
@@ -1,35 +1,59 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandStashDo is a function.
|
||||
func TestGitCommandStashDo(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"stash", "drop", "stash@{1}"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}
|
||||
runner := oscommands.NewFakeRunner(t).
|
||||
ExpectArgs([]string{"git", "stash", "drop", "stash@{1}"}, "", nil)
|
||||
gitCmd := NewDummyGitCommandWithRunner(runner)
|
||||
|
||||
assert.NoError(t, gitCmd.StashDo(1, "drop"))
|
||||
runner.CheckForMissingCalls()
|
||||
}
|
||||
|
||||
// TestGitCommandStashSave is a function.
|
||||
func TestGitCommandStashSave(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"stash", "save", "A stash message"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}
|
||||
runner := oscommands.NewFakeRunner(t).
|
||||
ExpectArgs([]string{"git", "stash", "save", "A stash message"}, "", nil)
|
||||
gitCmd := NewDummyGitCommandWithRunner(runner)
|
||||
|
||||
assert.NoError(t, gitCmd.StashSave("A stash message"))
|
||||
runner.CheckForMissingCalls()
|
||||
}
|
||||
|
||||
func TestGitCommandShowStashEntryCmdStr(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
index int
|
||||
contextSize int
|
||||
expected string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Default case",
|
||||
index: 5,
|
||||
contextSize: 3,
|
||||
expected: "git stash show -p --stat --color=always --unified=3 stash@{5}",
|
||||
},
|
||||
{
|
||||
testName: "Show diff with custom context size",
|
||||
index: 5,
|
||||
contextSize: 77,
|
||||
expected: "git stash show -p --stat --color=always --unified=77 stash@{5}",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.UserConfig.Git.DiffContextSize = s.contextSize
|
||||
cmdStr := gitCmd.ShowStashEntryCmdStr(s.index)
|
||||
assert.Equal(t, s.expected, cmdStr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,43 +4,37 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
gogit "github.com/jesseduffield/go-git/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
REBASE_MODE_NORMAL = "normal"
|
||||
REBASE_MODE_INTERACTIVE = "interactive"
|
||||
REBASE_MODE_REBASING = "rebasing"
|
||||
REBASE_MODE_MERGING = "merging"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
|
||||
)
|
||||
|
||||
// RebaseMode returns "" for non-rebase mode, "normal" for normal rebase
|
||||
// and "interactive" for interactive rebase
|
||||
func (c *GitCommand) RebaseMode() (string, error) {
|
||||
func (c *GitCommand) RebaseMode() (enums.RebaseMode, error) {
|
||||
exists, err := c.OSCommand.FileExists(filepath.Join(c.DotGitDir, "rebase-apply"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
return enums.REBASE_MODE_NONE, err
|
||||
}
|
||||
if exists {
|
||||
return REBASE_MODE_NORMAL, nil
|
||||
return enums.REBASE_MODE_NORMAL, nil
|
||||
}
|
||||
exists, err = c.OSCommand.FileExists(filepath.Join(c.DotGitDir, "rebase-merge"))
|
||||
if exists {
|
||||
return REBASE_MODE_INTERACTIVE, err
|
||||
return enums.REBASE_MODE_INTERACTIVE, err
|
||||
} else {
|
||||
return "", err
|
||||
return enums.REBASE_MODE_NONE, err
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GitCommand) WorkingTreeState() string {
|
||||
func (c *GitCommand) WorkingTreeState() enums.RebaseMode {
|
||||
rebaseMode, _ := c.RebaseMode()
|
||||
if rebaseMode != "" {
|
||||
return REBASE_MODE_REBASING
|
||||
if rebaseMode != enums.REBASE_MODE_NONE {
|
||||
return enums.REBASE_MODE_REBASING
|
||||
}
|
||||
merging, _ := c.IsInMergeState()
|
||||
if merging {
|
||||
return REBASE_MODE_MERGING
|
||||
return enums.REBASE_MODE_MERGING
|
||||
}
|
||||
return REBASE_MODE_NORMAL
|
||||
return enums.REBASE_MODE_NONE
|
||||
}
|
||||
|
||||
// IsInMergeState states whether we are still mid-merge
|
||||
|
||||
@@ -2,12 +2,15 @@ package commands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
)
|
||||
|
||||
// .gitmodules looks like this:
|
||||
@@ -15,7 +18,22 @@ import (
|
||||
// path = blah/mysubmodule
|
||||
// url = git@github.com:subbo.git
|
||||
|
||||
func (c *GitCommand) GetSubmoduleConfigs() ([]*models.SubmoduleConfig, error) {
|
||||
type SubmoduleCommands struct {
|
||||
*common.Common
|
||||
|
||||
cmd oscommands.ICmdObjBuilder
|
||||
dotGitDir string
|
||||
}
|
||||
|
||||
func NewSubmoduleCommands(common *common.Common, cmd oscommands.ICmdObjBuilder, dotGitDir string) SubmoduleCommands {
|
||||
return SubmoduleCommands{
|
||||
Common: common,
|
||||
cmd: cmd,
|
||||
dotGitDir: dotGitDir,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *SubmoduleCommands) GetConfigs() ([]*models.SubmoduleConfig, error) {
|
||||
file, err := os.Open(".gitmodules")
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
@@ -61,36 +79,36 @@ func (c *GitCommand) GetSubmoduleConfigs() ([]*models.SubmoduleConfig, error) {
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleStash(submodule *models.SubmoduleConfig) error {
|
||||
func (self *SubmoduleCommands) Stash(submodule *models.SubmoduleConfig) error {
|
||||
// if the path does not exist then it hasn't yet been initialized so we'll swallow the error
|
||||
// because the intention here is to have no dirty worktree state
|
||||
if _, err := os.Stat(submodule.Path); os.IsNotExist(err) {
|
||||
c.Log.Infof("submodule path %s does not exist, returning", submodule.Path)
|
||||
self.Log.Infof("submodule path %s does not exist, returning", submodule.Path)
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.RunCommand("git -C %s stash --include-untracked", submodule.Path)
|
||||
return self.cmd.New("git -C " + self.cmd.Quote(submodule.Path) + " stash --include-untracked").Run()
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleReset(submodule *models.SubmoduleConfig) error {
|
||||
return c.RunCommand("git submodule update --init --force %s", submodule.Path)
|
||||
func (self *SubmoduleCommands) Reset(submodule *models.SubmoduleConfig) error {
|
||||
return self.cmd.New("git submodule update --init --force -- " + self.cmd.Quote(submodule.Path)).Run()
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleUpdateAll() error {
|
||||
func (self *SubmoduleCommands) UpdateAll() error {
|
||||
// not doing an --init here because the user probably doesn't want that
|
||||
return c.RunCommand("git submodule update --force")
|
||||
return self.cmd.New("git submodule update --force").Run()
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleDelete(submodule *models.SubmoduleConfig) error {
|
||||
func (self *SubmoduleCommands) Delete(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 := self.cmd.New("git submodule deinit --force -- " + self.cmd.Quote(submodule.Path)).Run(); 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 := self.cmd.New("git config --file .gitmodules --remove-section submodule." + self.cmd.Quote(submodule.Name)).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.RunCommand("git config --remove-section submodule.%s", submodule.Name); err != nil {
|
||||
if err := self.cmd.New("git config --remove-section submodule." + self.cmd.Quote(submodule.Name)).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -100,66 +118,69 @@ func (c *GitCommand) SubmoduleDelete(submodule *models.SubmoduleConfig) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.RunCommand("git rm --force -r %s", submodule.Path); err != nil {
|
||||
if err := self.cmd.New("git rm --force -r " + submodule.Path).Run(); err != nil {
|
||||
// if the directory isn't there then that's fine
|
||||
c.Log.Error(err)
|
||||
self.Log.Error(err)
|
||||
}
|
||||
|
||||
return os.RemoveAll(filepath.Join(c.DotGitDir, "modules", submodule.Path))
|
||||
return os.RemoveAll(filepath.Join(self.dotGitDir, "modules", submodule.Path))
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleAdd(name string, path string, url string) error {
|
||||
return c.OSCommand.RunCommand(
|
||||
"git submodule add --force --name %s -- %s %s ",
|
||||
c.OSCommand.Quote(name),
|
||||
c.OSCommand.Quote(url),
|
||||
c.OSCommand.Quote(path),
|
||||
)
|
||||
func (self *SubmoduleCommands) Add(name string, path string, url string) error {
|
||||
return self.cmd.
|
||||
New(
|
||||
fmt.Sprintf(
|
||||
"git submodule add --force --name %s -- %s %s ",
|
||||
self.cmd.Quote(name),
|
||||
self.cmd.Quote(url),
|
||||
self.cmd.Quote(path),
|
||||
)).
|
||||
Run()
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleUpdateUrl(name string, path string, newUrl string) error {
|
||||
func (self *SubmoduleCommands) UpdateUrl(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 := self.cmd.New("git config --file .gitmodules submodule." + self.cmd.Quote(name) + ".url " + self.cmd.Quote(newUrl)).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.RunCommand("git submodule sync %s", path); err != nil {
|
||||
if err := self.cmd.New("git submodule sync -- " + self.cmd.Quote(path)).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleInit(path string) error {
|
||||
return c.RunCommand("git submodule init %s", path)
|
||||
func (self *SubmoduleCommands) Init(path string) error {
|
||||
return self.cmd.New("git submodule init -- " + self.cmd.Quote(path)).Run()
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleUpdate(path string) error {
|
||||
return c.RunCommand("git submodule update --init %s", path)
|
||||
func (self *SubmoduleCommands) Update(path string) error {
|
||||
return self.cmd.New("git submodule update --init -- " + self.cmd.Quote(path)).Run()
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleBulkInitCmdStr() string {
|
||||
return "git submodule init"
|
||||
func (self *SubmoduleCommands) BulkInitCmdObj() oscommands.ICmdObj {
|
||||
return self.cmd.New("git submodule init")
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleBulkUpdateCmdStr() string {
|
||||
return "git submodule update"
|
||||
func (self *SubmoduleCommands) BulkUpdateCmdObj() oscommands.ICmdObj {
|
||||
return self.cmd.New("git submodule update")
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleForceBulkUpdateCmdStr() string {
|
||||
return "git submodule update --force"
|
||||
func (self *SubmoduleCommands) ForceBulkUpdateCmdObj() oscommands.ICmdObj {
|
||||
return self.cmd.New("git submodule update --force")
|
||||
}
|
||||
|
||||
func (c *GitCommand) SubmoduleBulkDeinitCmdStr() string {
|
||||
return "git submodule deinit --all --force"
|
||||
func (self *SubmoduleCommands) BulkDeinitCmdObj() oscommands.ICmdObj {
|
||||
return self.cmd.New("git submodule deinit --all --force")
|
||||
}
|
||||
|
||||
func (c *GitCommand) ResetSubmodules(submodules []*models.SubmoduleConfig) error {
|
||||
func (self *SubmoduleCommands) ResetSubmodules(submodules []*models.SubmoduleConfig) error {
|
||||
for _, submodule := range submodules {
|
||||
if err := c.SubmoduleStash(submodule); err != nil {
|
||||
if err := self.Stash(submodule); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return c.SubmoduleUpdateAll()
|
||||
return self.UpdateAll()
|
||||
}
|
||||
|
||||
@@ -2,27 +2,52 @@ package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
func (c *GitCommand) PushCmdObj(opts PushOpts) (oscommands.ICmdObj, 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 nil, errors.New(c.Tr.MustSpecifyOriginError)
|
||||
}
|
||||
cmdStr += " " + c.OSCommand.Quote(opts.UpstreamBranch)
|
||||
}
|
||||
|
||||
cmdObj := c.Cmd.New(cmdStr)
|
||||
return cmdObj, nil
|
||||
}
|
||||
|
||||
func (c *GitCommand) Push(opts PushOpts, promptUserForCredential func(string) string) error {
|
||||
cmdObj, err := c.PushCmdObj(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.DetectUnamePass(cmdObj, promptUserForCredential)
|
||||
}
|
||||
|
||||
type FetchOptions struct {
|
||||
@@ -33,16 +58,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.Cmd.New(cmdStr)
|
||||
return c.DetectUnamePass(cmdObj, func(question string) string {
|
||||
if opts.PromptUserForCredential != nil {
|
||||
return opts.PromptUserForCredential(question)
|
||||
}
|
||||
@@ -50,12 +76,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.Cmd.New(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.Cmd.New(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.Cmd.New(cmdStr)
|
||||
return c.DetectUnamePass(cmdObj, promptUserForCredential)
|
||||
}
|
||||
|
||||
@@ -1,98 +1,93 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// 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
|
||||
opts PushOpts
|
||||
test func(oscommands.ICmdObj, error)
|
||||
}
|
||||
|
||||
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) {
|
||||
testName: "Push with force disabled",
|
||||
opts: PushOpts{Force: false},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.ToString(), "git push")
|
||||
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) {
|
||||
testName: "Push with force enabled",
|
||||
opts: PushOpts{Force: true},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.ToString(), "git push --force-with-lease")
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with force disabled, follow-tags off",
|
||||
func(string) (string, error) {
|
||||
return "false", nil
|
||||
testName: "Push with force disabled, upstream supplied",
|
||||
opts: PushOpts{
|
||||
Force: false,
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "master",
|
||||
},
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
false,
|
||||
func(err error) {
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.ToString(), `git push "origin" "master"`)
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with an error occurring, follow-tags on",
|
||||
func(string) (string, error) {
|
||||
return "", nil
|
||||
testName: "Push with force disabled, setting upstream",
|
||||
opts: PushOpts{
|
||||
Force: false,
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "master",
|
||||
SetUpstream: true,
|
||||
},
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push", "--follow-tags"}, args)
|
||||
return secureexec.Command("test")
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.ToString(), `git push --set-upstream "origin" "master"`)
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
false,
|
||||
func(err error) {
|
||||
},
|
||||
{
|
||||
testName: "Push with force enabled, setting upstream",
|
||||
opts: PushOpts{
|
||||
Force: true,
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "master",
|
||||
SetUpstream: true,
|
||||
},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.ToString(), `git push --force-with-lease --set-upstream "origin" "master"`)
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Push with remote branch but no origin",
|
||||
opts: PushOpts{
|
||||
Force: true,
|
||||
UpstreamRemote: "",
|
||||
UpstreamBranch: "master",
|
||||
SetUpstream: true,
|
||||
},
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.EqualValues(t, "Must specify a remote if specifying a branch", err.Error())
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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"
|
||||
})
|
||||
s.test(err)
|
||||
gitCmd := NewDummyGitCommandWithRunner(oscommands.NewFakeRunner(t))
|
||||
s.test(gitCmd.PushCmdObj(s.opts))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
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.Cmd.New(fmt.Sprintf("git tag -- %s %s", c.OSCommand.Quote(tagName), commitSha)).Run()
|
||||
}
|
||||
|
||||
func (c *GitCommand) CreateAnnotatedTag(tagName, commitSha, msg string) error {
|
||||
return c.Cmd.New(fmt.Sprintf("git tag %s %s -m %s", tagName, commitSha, c.OSCommand.Quote(msg))).Run()
|
||||
}
|
||||
|
||||
func (c *GitCommand) DeleteTag(tagName string) error {
|
||||
return c.RunCommand("git tag -d %s", tagName)
|
||||
return c.Cmd.New(fmt.Sprintf("git tag -d %s", c.OSCommand.Quote(tagName))).Run()
|
||||
}
|
||||
|
||||
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.Cmd.New(cmdStr)
|
||||
return c.DetectUnamePass(cmdObj, promptUserForCredential)
|
||||
}
|
||||
|
||||
14
pkg/commands/types/enums/enums.go
Normal file
14
pkg/commands/types/enums/enums.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package enums
|
||||
|
||||
type RebaseMode int
|
||||
|
||||
const (
|
||||
// this means we're neither rebasing nor merging
|
||||
REBASE_MODE_NONE RebaseMode = iota
|
||||
// this means normal rebase as opposed to interactive rebase
|
||||
REBASE_MODE_NORMAL
|
||||
REBASE_MODE_INTERACTIVE
|
||||
// REBASE_MODE_REBASING is a general state that captures both REBASE_MODE_NORMAL and REBASE_MODE_INTERACTIVE
|
||||
REBASE_MODE_REBASING
|
||||
REBASE_MODE_MERGING
|
||||
)
|
||||
15
pkg/common/common.go
Normal file
15
pkg/common/common.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Commonly used things wrapped into one struct for convenience when passing it around
|
||||
type Common struct {
|
||||
Log *logrus.Entry
|
||||
Tr *i18n.TranslationSet
|
||||
UserConfig *config.UserConfig
|
||||
Debug bool
|
||||
}
|
||||
@@ -12,46 +12,60 @@ 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
|
||||
// from AppConfig and still be used by lazygit.
|
||||
type AppConfigurer interface {
|
||||
GetDebug() bool
|
||||
|
||||
// build info
|
||||
GetVersion() string
|
||||
GetCommit() string
|
||||
GetBuildDate() string
|
||||
GetName() string
|
||||
GetBuildSource() string
|
||||
|
||||
GetUserConfig() *UserConfig
|
||||
GetUserConfigPaths() []string
|
||||
GetUserConfigDir() string
|
||||
GetUserConfigPath() string
|
||||
ReloadUserConfig() error
|
||||
|
||||
GetAppState() *AppState
|
||||
SaveAppState() error
|
||||
SetIsNewRepo(bool)
|
||||
GetIsNewRepo() bool
|
||||
ReloadUserConfig() error
|
||||
}
|
||||
|
||||
// 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,79 +123,66 @@ 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
|
||||
}
|
||||
|
||||
// GetIsNewRepo returns known repo boolean
|
||||
func (c *AppConfig) GetIsNewRepo() bool {
|
||||
return c.IsNewRepo
|
||||
}
|
||||
|
||||
// SetIsNewRepo set if the current repo is known
|
||||
func (c *AppConfig) SetIsNewRepo(toSet bool) {
|
||||
c.IsNewRepo = toSet
|
||||
}
|
||||
|
||||
// GetDebug returns debug flag
|
||||
func (c *AppConfig) GetDebug() bool {
|
||||
return c.Debug
|
||||
}
|
||||
|
||||
// GetVersion returns debug flag
|
||||
func (c *AppConfig) GetVersion() string {
|
||||
return c.Version
|
||||
}
|
||||
|
||||
// GetCommit returns debug flag
|
||||
func (c *AppConfig) GetCommit() string {
|
||||
return c.Commit
|
||||
}
|
||||
|
||||
// GetBuildDate returns debug flag
|
||||
func (c *AppConfig) GetBuildDate() string {
|
||||
return c.BuildDate
|
||||
}
|
||||
|
||||
// GetName returns debug flag
|
||||
func (c *AppConfig) GetName() string {
|
||||
return c.Name
|
||||
}
|
||||
@@ -190,22 +198,21 @@ 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) ReloadUserConfig() error {
|
||||
userConfig, err := loadUserConfigWithDefaults(c.UserConfigDir)
|
||||
userConfig, err := loadUserConfigWithDefaults(c.UserConfigPaths)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -223,9 +230,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 +249,23 @@ 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
|
||||
}
|
||||
|
||||
// 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 +293,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
|
||||
|
||||
@@ -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 {
|
||||
@@ -56,8 +61,8 @@ type CommitLengthConfig struct {
|
||||
|
||||
type GitConfig struct {
|
||||
Paging PagingConfig `yaml:"paging"`
|
||||
Commit CommitConfig `yaml:"commit"`
|
||||
Merging MergingConfig `yaml:"merging"`
|
||||
Pull PullConfig `yaml:"pull"`
|
||||
SkipHookPrefix string `yaml:"skipHookPrefix"`
|
||||
AutoFetch bool `yaml:"autoFetch"`
|
||||
BranchLogCmd string `yaml:"branchLogCmd"`
|
||||
@@ -65,6 +70,10 @@ 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"`
|
||||
DiffContextSize int `yaml:"diffContextSize"`
|
||||
}
|
||||
|
||||
type PagingConfig struct {
|
||||
@@ -73,13 +82,18 @@ type PagingConfig struct {
|
||||
UseConfig bool `yaml:"useConfig"`
|
||||
}
|
||||
|
||||
type CommitConfig struct {
|
||||
SignOff bool `yaml:"signOff"`
|
||||
}
|
||||
|
||||
type MergingConfig struct {
|
||||
ManualCommit bool `yaml:"manualCommit"`
|
||||
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 +120,70 @@ 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"`
|
||||
IncreaseContextInDiffView string `yaml:"increaseContextInDiffView"`
|
||||
DecreaseContextInDiffView string `yaml:"decreaseContextInDiffView"`
|
||||
}
|
||||
|
||||
type KeybindingStatusConfig struct {
|
||||
@@ -185,10 +206,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 +245,8 @@ type KeybindingCommitsConfig struct {
|
||||
CheckoutCommit string `yaml:"checkoutCommit"`
|
||||
ResetCherryPick string `yaml:"resetCherryPick"`
|
||||
CopyCommitMessageToClipboard string `yaml:"copyCommitMessageToClipboard"`
|
||||
OpenLogMenu string `yaml:"openLogMenu"`
|
||||
OpenInBrowser string `yaml:"openInBrowser"`
|
||||
}
|
||||
|
||||
type KeybindingStashConfig struct {
|
||||
@@ -247,6 +272,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 +304,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 +329,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,
|
||||
},
|
||||
@@ -312,12 +353,16 @@ func GetDefaultConfig() *UserConfig {
|
||||
ColorArg: "always",
|
||||
Pager: "",
|
||||
UseConfig: false},
|
||||
Commit: CommitConfig{
|
||||
SignOff: false,
|
||||
},
|
||||
Merging: MergingConfig{
|
||||
ManualCommit: false,
|
||||
Args: "",
|
||||
},
|
||||
Pull: PullConfig{
|
||||
Mode: "merge",
|
||||
Log: LogConfig{
|
||||
Order: "topo-order",
|
||||
ShowGraph: "when-maximised",
|
||||
},
|
||||
SkipHookPrefix: "WIP",
|
||||
AutoFetch: true,
|
||||
@@ -325,6 +370,8 @@ 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,
|
||||
DiffContextSize: 3,
|
||||
},
|
||||
Refresher: RefresherConfig{
|
||||
RefreshInterval: 10,
|
||||
@@ -351,6 +398,8 @@ func GetDefaultConfig() *UserConfig {
|
||||
NextItemAlt: "j",
|
||||
PrevPage: ",",
|
||||
NextPage: ".",
|
||||
ScrollLeft: "H",
|
||||
ScrollRight: "L",
|
||||
GotoTop: "<",
|
||||
GotoBottom: ">",
|
||||
PrevBlock: "<left>",
|
||||
@@ -359,6 +408,7 @@ func GetDefaultConfig() *UserConfig {
|
||||
NextBlockAlt: "l",
|
||||
PrevBlockAlt2: "<backtab>",
|
||||
NextBlockAlt2: "<tab>",
|
||||
JumpToBlock: []string{"1", "2", "3", "4", "5"},
|
||||
NextMatch: "n",
|
||||
PrevMatch: "N",
|
||||
StartSearch: "/",
|
||||
@@ -372,6 +422,7 @@ func GetDefaultConfig() *UserConfig {
|
||||
New: "n",
|
||||
Edit: "e",
|
||||
OpenFile: "o",
|
||||
OpenRecentRepos: "<c-r>",
|
||||
ScrollUpMain: "<pgup>",
|
||||
ScrollDownMain: "<pgdown>",
|
||||
ScrollUpMainAlt1: "K",
|
||||
@@ -397,6 +448,9 @@ func GetDefaultConfig() *UserConfig {
|
||||
SubmitEditorText: "<enter>",
|
||||
AppendNewline: "<a-enter>",
|
||||
ExtrasMenu: "@",
|
||||
ToggleWhitespaceInDiffView: "<c-w>",
|
||||
IncreaseContextInDiffView: "}",
|
||||
DecreaseContextInDiffView: "{",
|
||||
},
|
||||
Status: KeybindingStatusConfig{
|
||||
CheckForUpdate: "u",
|
||||
@@ -417,10 +471,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 +508,8 @@ func GetDefaultConfig() *UserConfig {
|
||||
CheckoutCommit: "<space>",
|
||||
ResetCherryPick: "<c-R>",
|
||||
CopyCommitMessageToClipboard: "<c-y>",
|
||||
OpenLogMenu: "<c-l>",
|
||||
OpenInBrowser: "o",
|
||||
},
|
||||
Stash: KeybindingStashConfig{
|
||||
PopStash: "g",
|
||||
|
||||
@@ -44,7 +44,7 @@ func (gui *Gui) getMidSectionWeights() (int, int) {
|
||||
currentWindow := gui.currentWindow()
|
||||
|
||||
// we originally specified this as a ratio i.e. .20 would correspond to a weight of 1 against 4
|
||||
sidePanelWidthRatio := gui.Config.GetUserConfig().Gui.SidePanelWidth
|
||||
sidePanelWidthRatio := gui.UserConfig.Gui.SidePanelWidth
|
||||
// we could make this better by creating ratios like 2:3 rather than always 1:something
|
||||
mainSectionWeight := int(1/sidePanelWidthRatio) - 1
|
||||
sideSectionWeight := 1
|
||||
@@ -115,7 +115,7 @@ func (gui *Gui) splitMainPanelSideBySide() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
mainPanelSplitMode := gui.Config.GetUserConfig().Gui.MainPanelSplitMode
|
||||
mainPanelSplitMode := gui.UserConfig.Gui.MainPanelSplitMode
|
||||
width, height := gui.g.Size()
|
||||
|
||||
switch mainPanelSplitMode {
|
||||
@@ -143,7 +143,7 @@ func (gui *Gui) getExtrasWindowSize(screenHeight int) int {
|
||||
} else if screenHeight < 40 {
|
||||
baseSize = 1
|
||||
} else {
|
||||
baseSize = gui.Config.GetUserConfig().Gui.CommandLogSize
|
||||
baseSize = gui.UserConfig.Gui.CommandLogSize
|
||||
}
|
||||
|
||||
frameSize := 2
|
||||
@@ -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.UserConfig.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
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package gui
|
||||
|
||||
type BasicContext struct {
|
||||
OnFocus func() error
|
||||
OnFocusLost func() error
|
||||
OnRender func() error
|
||||
OnFocus func(opts ...OnFocusOpts) error
|
||||
OnFocusLost func() error
|
||||
OnRender func() error
|
||||
// this is for pushing some content to the main view
|
||||
OnRenderToMain func(opts ...OnFocusOpts) error
|
||||
Kind ContextKind
|
||||
Key ContextKey
|
||||
ViewName string
|
||||
@@ -15,63 +17,83 @@ type BasicContext struct {
|
||||
hasParent bool
|
||||
}
|
||||
|
||||
func (c *BasicContext) GetOptionsMap() map[string]string {
|
||||
if c.OnGetOptionsMap != nil {
|
||||
return c.OnGetOptionsMap()
|
||||
func (self *BasicContext) GetOptionsMap() map[string]string {
|
||||
if self.OnGetOptionsMap != nil {
|
||||
return self.OnGetOptionsMap()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *BasicContext) SetParentContext(context Context) {
|
||||
c.ParentContext = context
|
||||
c.hasParent = true
|
||||
func (self *BasicContext) SetParentContext(context Context) {
|
||||
self.ParentContext = context
|
||||
self.hasParent = true
|
||||
}
|
||||
|
||||
func (c *BasicContext) GetParentContext() (Context, bool) {
|
||||
return c.ParentContext, c.hasParent
|
||||
func (self *BasicContext) GetParentContext() (Context, bool) {
|
||||
return self.ParentContext, self.hasParent
|
||||
}
|
||||
|
||||
func (c *BasicContext) SetWindowName(windowName string) {
|
||||
c.WindowName = windowName
|
||||
func (self *BasicContext) SetWindowName(windowName string) {
|
||||
self.WindowName = windowName
|
||||
}
|
||||
|
||||
func (c *BasicContext) GetWindowName() string {
|
||||
windowName := c.WindowName
|
||||
func (self *BasicContext) GetWindowName() string {
|
||||
windowName := self.WindowName
|
||||
|
||||
if windowName != "" {
|
||||
return windowName
|
||||
}
|
||||
|
||||
// TODO: actually set this for everything so we don't default to the view name
|
||||
return c.ViewName
|
||||
return self.ViewName
|
||||
}
|
||||
|
||||
func (c *BasicContext) HandleRender() error {
|
||||
if c.OnRender != nil {
|
||||
return c.OnRender()
|
||||
func (self *BasicContext) HandleRender() error {
|
||||
if self.OnRender != nil {
|
||||
return self.OnRender()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *BasicContext) GetViewName() string {
|
||||
return c.ViewName
|
||||
func (self *BasicContext) GetViewName() string {
|
||||
return self.ViewName
|
||||
}
|
||||
|
||||
func (c *BasicContext) HandleFocus() error {
|
||||
return c.OnFocus()
|
||||
func (self *BasicContext) HandleFocus(opts ...OnFocusOpts) error {
|
||||
if self.OnFocus != nil {
|
||||
if err := self.OnFocus(opts...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if self.OnRenderToMain != nil {
|
||||
if err := self.OnRenderToMain(opts...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *BasicContext) HandleFocusLost() error {
|
||||
if c.OnFocusLost != nil {
|
||||
return c.OnFocusLost()
|
||||
func (self *BasicContext) HandleFocusLost() error {
|
||||
if self.OnFocusLost != nil {
|
||||
return self.OnFocusLost()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *BasicContext) GetKind() ContextKind {
|
||||
return c.Kind
|
||||
func (self *BasicContext) HandleRenderToMain() error {
|
||||
if self.OnRenderToMain != nil {
|
||||
return self.OnRenderToMain()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *BasicContext) GetKey() ContextKey {
|
||||
return c.Key
|
||||
func (self *BasicContext) GetKind() ContextKind {
|
||||
return self.Kind
|
||||
}
|
||||
|
||||
func (self *BasicContext) GetKey() ContextKey {
|
||||
return self.Key
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user