mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-28 10:16:22 -04:00
Compare 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 |
@@ -21,11 +21,13 @@ If you're a mere mortal like me and you're tired of hearing how powerful git is
|
||||
- [Binary releases](#binary-releases)
|
||||
- [Homebrew](#homebrew)
|
||||
- [MacPorts](#macports)
|
||||
- [Ubuntu](#ubuntu)
|
||||
- [Void Linux](#void-linux)
|
||||
- [Scoop (Windows)](#scoop-windows)
|
||||
- [Arch Linux](#arch-linux)
|
||||
- [Fedora and CentOS 7](#fedora-and-centos-7)
|
||||
- [Solus Linux](#solus-linux)
|
||||
- [Funtoo Linux](#funtoo-linux)
|
||||
- [FreeBSD](#freebsd)
|
||||
- [Conda](#conda)
|
||||
- [Go](#go)
|
||||
@@ -141,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
|
||||
@@ -256,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
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +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
|
||||
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)
|
||||
|
||||
+40
-2
@@ -45,7 +45,7 @@ gui:
|
||||
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
|
||||
@@ -54,11 +54,21 @@ 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: ''
|
||||
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}} --'
|
||||
@@ -66,6 +76,7 @@ git:
|
||||
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}}'
|
||||
@@ -97,11 +108,13 @@ 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
|
||||
jumpToBlock: ['1', '2', '3', '4', '5'] # goto the Nth block / panel
|
||||
nextMatch: 'n'
|
||||
prevMatch: 'N'
|
||||
optionMenu: 'x' # show help menu
|
||||
@@ -141,6 +154,8 @@ keybinding:
|
||||
appendNewline: '<a-enter>'
|
||||
extrasMenu: '@'
|
||||
toggleWhitespaceInDiffView: '<c-w>'
|
||||
increaseContextInDiffView: '}'
|
||||
decreaseContextInDiffView: '{'
|
||||
status:
|
||||
checkForUpdate: 'u'
|
||||
recentRepos: '<enter>'
|
||||
@@ -189,6 +204,7 @@ keybinding:
|
||||
checkoutCommit: '<space>'
|
||||
resetCherryPick: '<c-R>'
|
||||
copyCommitMessageToClipboard: '<c-y>'
|
||||
openLogMenu: '<c-l>'
|
||||
stash:
|
||||
popStash: 'g'
|
||||
commitFiles:
|
||||
@@ -360,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
|
||||
|
||||

|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
<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
|
||||
|
||||
@@ -9,33 +9,30 @@ 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/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fatih/color v1.9.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.4.7
|
||||
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/iriri/minimal/gitignore v0.3.2 // indirect
|
||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20211017220056-b2fc03c74a6f
|
||||
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/kyokomi/emoji/v2 v2.2.8
|
||||
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.13 // indirect
|
||||
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/ozeidan/fuzzy-patricia v3.0.0+incompatible // indirect
|
||||
github.com/sahilm/fuzzy v0.1.0
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad
|
||||
@@ -43,7 +40,7 @@ require (
|
||||
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-20211015200801-69063c4bb744 // 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
|
||||
|
||||
@@ -56,8 +56,8 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
|
||||
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=
|
||||
@@ -67,24 +67,12 @@ github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
|
||||
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/integrii/flaggy v1.4.0 h1:A1x7SYx4jqu5NSrY14z8Z+0UyX2S5ygfJJrfolWR3zM=
|
||||
github.com/integrii/flaggy v1.4.0/go.mod h1:tnTxHeTJbah0gQ6/K0RW0J7fMUBk9MCF5blhm43LNpI=
|
||||
github.com/iriri/minimal v0.0.0-20180828191352-9b2348d09c1a h1:mCZYG6QcX0dz/J0rFc1tcRYGeixlDcCGSPXuPMbiS5U=
|
||||
github.com/iriri/minimal/gitignore v0.3.2 h1:MnTVH89iuwiyZ/a1pByw/mAU2ShWai1yvv0tgHSq5Ww=
|
||||
github.com/iriri/minimal/gitignore v0.3.2/go.mod h1:v7YhsYBAInyAnQligwCIGRuQmtwQyYxkVy5vEdy2wPU=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4 h1:GOQrmaE8i+KEdB8NzAegKYd4tPn/inM0I1uo0NXFerg=
|
||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20211017035223-b68948e63cc3 h1:J5s/4Y860tas8J0AMQ3gJKCbJPx8zNpiTm5UjEgPQfY=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20211017035223-b68948e63cc3/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20211017041119-0ec562dfd23b h1:kepukaDQfZ6LBSvHUYReFvVSW5Lx5ZQZDgGhXj0Mx7U=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20211017041119-0ec562dfd23b/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20211017063715-c74848d8ad00 h1:5TusU8ir9OHg3By2PPmLwa2y+2G9F+16QRK8bpofsC0=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20211017063715-c74848d8ad00/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20211017091015-8bf4a4666b77 h1:MQUxSxVBTZQpSYybEiFA4+oIi02ycTKGCqgHItYi/20=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20211017091015-8bf4a4666b77/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20211017220056-b2fc03c74a6f h1:JHrb78pj+gYC3KiJKL1WW6lYzlatBIF46oREn68plTM=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20211017220056-b2fc03c74a6f/go.mod h1:znJuCDnF2Ph40YZSlBwdX/4GEofnIoWLGdT4mK5zRAU=
|
||||
github.com/jesseduffield/minimal v0.0.0-20211018110810-9cde264e6b1e h1:WZc73tBVMMhcO6zXyZBItLEF4jgBpBH0lFCZzDgrjDg=
|
||||
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=
|
||||
@@ -113,13 +101,13 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69
|
||||
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/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/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-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.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
@@ -134,9 +122,6 @@ github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY=
|
||||
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/ozeidan/fuzzy-patricia v1.0.1 h1:YExnavqXH3OvCCqE2TunuJJHdFcFQdVEfUoWzrnPxSg=
|
||||
github.com/ozeidan/fuzzy-patricia v3.0.0+incompatible h1:Pl61eMyfJqgY/wytiI4vamqPYribq6d8VxeP1CNyg9M=
|
||||
github.com/ozeidan/fuzzy-patricia v3.0.0+incompatible/go.mod h1:zgvuCcYS7wB7fVCGblsaFFmEe8+aAH13dTYm8FbrpsM=
|
||||
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=
|
||||
@@ -186,15 +171,15 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-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/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-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-20211015200801-69063c4bb744 h1:KzbpndAYEM+4oHRp9JmB2ewj0NHHxO3Z0g7Gus2O1kk=
|
||||
golang.org/x/sys v0.0.0-20211015200801-69063c4bb744/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-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -203,6 +188,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
|
||||
+24
-13
@@ -17,6 +17,7 @@ import (
|
||||
"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"
|
||||
@@ -27,14 +28,12 @@ import (
|
||||
|
||||
// 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,27 +96,35 @@ 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, err = i18n.NewTranslationSetFromConfig(app.Log, config.GetUserConfig().Gui.Language)
|
||||
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")
|
||||
if app.ClientContext != "" {
|
||||
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
|
||||
}
|
||||
@@ -127,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, git_config.NewStdCachedGitConfig(app.Log))
|
||||
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
|
||||
}
|
||||
@@ -140,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 {
|
||||
@@ -197,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)
|
||||
@@ -225,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
|
||||
}
|
||||
}
|
||||
|
||||
+36
-24
@@ -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", c.OSCommand.Quote(name), c.OSCommand.Quote(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, c.OSCommand.Quote(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, c.OSCommand.Quote(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}", c.OSCommand.Quote(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": 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", c.OSCommand.Quote(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", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(remoteBranchName), c.OSCommand.Quote(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, c.OSCommand.Quote(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 " + c.OSCommand.Quote(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 " + c.OSCommand.Quote(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 " + c.OSCommand.Quote(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", c.OSCommand.Quote(oldName), c.OSCommand.Quote(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()
|
||||
}
|
||||
|
||||
+69
-157
@@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
+26
-19
@@ -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,51 +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.RunCommandWithOutput("git show --no-patch --pretty=format:%%s %s", sha)
|
||||
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.RunCommand("git revert %s -m %d", sha, parentNumber)
|
||||
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
|
||||
@@ -84,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,31 @@
|
||||
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 {
|
||||
@@ -70,43 +58,77 @@ func TestGitCommandCommitStr(t *testing.T) {
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ func (c *GitCommand) ConfiguredPager() string {
|
||||
}
|
||||
|
||||
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]
|
||||
@@ -30,18 +30,18 @@ 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
|
||||
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
|
||||
}
|
||||
|
||||
+16
-8
@@ -1,10 +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,12 +16,19 @@ func NewDummyGitCommand() *GitCommand {
|
||||
|
||||
// NewDummyGitCommandWithOSCommand creates a new dummy GitCommand for testing
|
||||
func NewDummyGitCommandWithOSCommand(osCommand *oscommands.OSCommand) *GitCommand {
|
||||
newAppConfig := config.NewDummyAppConfig()
|
||||
return &GitCommand{
|
||||
Log: utils.NewDummyLog(),
|
||||
OSCommand: osCommand,
|
||||
Tr: i18n.NewTranslationSet(utils.NewDummyLog(), newAppConfig.GetUserConfig().Gui.Language),
|
||||
Config: newAppConfig,
|
||||
GitConfig: git_config.NewFakeGitConfig(map[string]string{}),
|
||||
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
|
||||
}
|
||||
|
||||
+42
-33
@@ -9,7 +9,9 @@ import (
|
||||
"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"
|
||||
)
|
||||
@@ -23,27 +25,27 @@ func (c *GitCommand) CatFile(fileName string) (string, error) {
|
||||
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
|
||||
@@ -56,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
|
||||
}
|
||||
}
|
||||
@@ -73,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 {
|
||||
@@ -120,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
|
||||
}
|
||||
}
|
||||
@@ -161,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
|
||||
}
|
||||
|
||||
@@ -186,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
|
||||
@@ -197,16 +203,17 @@ 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, ignoreWhitespace bool) string {
|
||||
// for now we assume an error means the file was deleted
|
||||
s, _ := c.OSCommand.RunCommandWithOutput(c.WorktreeFileDiffCmdStr(file, plain, cached, ignoreWhitespace))
|
||||
s, _ := c.WorktreeFileDiffCmdObj(file, plain, cached, ignoreWhitespace).RunWithOutput()
|
||||
return s
|
||||
}
|
||||
|
||||
func (c *GitCommand) WorktreeFileDiffCmdStr(node models.IFile, plain bool, cached bool, ignoreWhitespace bool) string {
|
||||
func (c *GitCommand) WorktreeFileDiffCmdObj(node models.IFile, plain bool, cached bool, ignoreWhitespace bool) oscommands.ICmdObj {
|
||||
cachedArg := ""
|
||||
trackedArg := "--"
|
||||
colorArg := c.colorArg()
|
||||
quotedPath := c.OSCommand.Quote(node.GetPath())
|
||||
ignoreWhitespaceArg := ""
|
||||
contextSize := c.UserConfig.Git.DiffContextSize
|
||||
if cached {
|
||||
cachedArg = "--cached"
|
||||
}
|
||||
@@ -220,11 +227,13 @@ func (c *GitCommand) WorktreeFileDiffCmdStr(node models.IFile, plain bool, cache
|
||||
ignoreWhitespaceArg = "--ignore-all-space"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("git diff --submodule --no-ext-diff --color=%s %s %s %s %s", colorArg, ignoreWhitespaceArg, cachedArg, trackedArg, quotedPath)
|
||||
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.GetTempDir(), 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
|
||||
@@ -235,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"
|
||||
}
|
||||
@@ -256,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, c.OSCommand.Quote(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, c.OSCommand.Quote(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
|
||||
@@ -271,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", c.OSCommand.Quote(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
|
||||
}
|
||||
@@ -294,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", c.OSCommand.Quote(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
|
||||
}
|
||||
}
|
||||
@@ -328,7 +337,7 @@ func (c *GitCommand) ResetAndClean() error {
|
||||
}
|
||||
|
||||
func (c *GitCommand) EditFileCmdStr(filename string, lineNumber int) (string, error) {
|
||||
editor := c.Config.GetUserConfig().OS.EditCommand
|
||||
editor := c.UserConfig.OS.EditCommand
|
||||
|
||||
if editor == "" {
|
||||
editor = c.GitConfig.Get("core.editor")
|
||||
@@ -344,7 +353,7 @@ func (c *GitCommand) EditFileCmdStr(filename string, lineNumber int) (string, er
|
||||
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"
|
||||
}
|
||||
}
|
||||
@@ -358,6 +367,6 @@ func (c *GitCommand) EditFileCmdStr(filename string, lineNumber int) (string, er
|
||||
"line": strconv.Itoa(lineNumber),
|
||||
}
|
||||
|
||||
editCmdTemplate := c.Config.GetUserConfig().OS.EditCommandTemplate
|
||||
editCmdTemplate := c.UserConfig.OS.EditCommandTemplate
|
||||
return utils.ResolvePlaceholderString(editCmdTemplate, templateValues), nil
|
||||
}
|
||||
|
||||
+357
-491
File diff suppressed because it is too large
Load Diff
+53
-45
@@ -1,23 +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:
|
||||
@@ -26,28 +25,45 @@ 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
|
||||
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,
|
||||
cmn *common.Common,
|
||||
osCommand *oscommands.OSCommand,
|
||||
tr *i18n.TranslationSet,
|
||||
config config.AppConfigurer,
|
||||
gitConfig git_config.IGitConfig,
|
||||
) (*GitCommand, error) {
|
||||
var repo *gogit.Repository
|
||||
@@ -59,7 +75,7 @@ func NewGitCommand(
|
||||
}
|
||||
|
||||
var err error
|
||||
if repo, err = setupRepository(gogit.PlainOpen, tr.GitconfigParseErr); err != nil {
|
||||
if repo, err = setupRepository(gogit.PlainOpen, cmn.Tr.GitconfigParseErr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -68,18 +84,33 @@ func NewGitCommand(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cmd := NewGitCmdObjBuilder(cmn.Log, osCommand.Cmd)
|
||||
|
||||
gitCommand := &GitCommand{
|
||||
Log: log,
|
||||
Common: cmn,
|
||||
OSCommand: osCommand,
|
||||
Tr: tr,
|
||||
Repo: repo,
|
||||
Config: config,
|
||||
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
|
||||
}
|
||||
@@ -96,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
|
||||
@@ -216,38 +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) NewCmdObjFromStr(cmdStr string) oscommands.ICmdObj {
|
||||
return c.OSCommand.NewCmdObjFromStr(cmdStr).AddEnvVars("GIT_OPTIONAL_LOCKS=0")
|
||||
func (c *GitCommand) GetCmd() oscommands.ICmdObjBuilder {
|
||||
return c.Cmd
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -10,8 +10,6 @@ import (
|
||||
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"
|
||||
)
|
||||
@@ -210,8 +208,7 @@ func TestNewGitCommand(t *testing.T) {
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
s.setup()
|
||||
newAppConfig := config.NewDummyAppConfig()
|
||||
s.test(NewGitCommand(utils.NewDummyLog(), oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog(), newAppConfig.GetUserConfig().Gui.Language), newAppConfig, git_config.NewFakeGitConfig(nil)))
|
||||
s.test(NewGitCommand(utils.NewDummyCommon(), oscommands.NewDummyOSCommand(), git_config.NewFakeGitConfig(nil)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -98,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:] {
|
||||
@@ -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,10 +1,9 @@
|
||||
package commands
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
@@ -12,9 +11,9 @@ import (
|
||||
|
||||
"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/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// 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]
|
||||
@@ -83,115 +194,66 @@ func (c *CommitListBuilder) extractCommitFromLine(line string) *models.Commit {
|
||||
}
|
||||
}
|
||||
|
||||
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)]
|
||||
youAreHere := style.FgYellow.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"))
|
||||
@@ -199,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
|
||||
@@ -211,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
|
||||
}
|
||||
@@ -242,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
|
||||
}
|
||||
@@ -276,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: ")
|
||||
@@ -287,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
|
||||
}
|
||||
@@ -310,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
|
||||
}
|
||||
@@ -322,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", c.OSCommand.Quote(refName), c.OSCommand.Quote(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
|
||||
}
|
||||
|
||||
@@ -338,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}", c.OSCommand.Quote(refName), c.OSCommand.Quote(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
|
||||
}
|
||||
@@ -348,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 %s",
|
||||
c.OSCommand.Quote(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:"
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,54 @@
|
||||
package commands
|
||||
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"
|
||||
)
|
||||
|
||||
// GetStatusFiles git status files
|
||||
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 (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
|
||||
func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
|
||||
// check if config wants us ignoring untracked files
|
||||
untrackedFilesSetting := c.GitConfig.Get("status.showUntrackedFiles")
|
||||
untrackedFilesSetting := self.gitConfig.Get("status.showUntrackedFiles")
|
||||
|
||||
if untrackedFilesSetting == "" {
|
||||
untrackedFilesSetting = "all"
|
||||
}
|
||||
untrackedFilesArg := fmt.Sprintf("--untracked-files=%s", untrackedFilesSetting)
|
||||
|
||||
statuses, err := c.GitStatus(GitStatusOptions{NoRenames: opts.NoRenames, UntrackedFilesArg: untrackedFilesArg})
|
||||
statuses, err := self.GitStatus(GitStatusOptions{NoRenames: opts.NoRenames, UntrackedFilesArg: untrackedFilesArg})
|
||||
if err != nil {
|
||||
c.Log.Error(err)
|
||||
self.Log.Error(err)
|
||||
}
|
||||
files := []*models.File{}
|
||||
|
||||
for _, status := range statuses {
|
||||
if strings.HasPrefix(status.StatusString, "warning") {
|
||||
c.Log.Warningf("warning when calling git status: %s", status.StatusString)
|
||||
self.Log.Warningf("warning when calling git status: %s", status.StatusString)
|
||||
continue
|
||||
}
|
||||
change := status.Change
|
||||
@@ -52,7 +70,7 @@ func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
|
||||
Added: unstagedChange == "A" || untracked,
|
||||
HasMergeConflicts: hasMergeConflicts,
|
||||
HasInlineMergeConflicts: hasInlineMergeConflicts,
|
||||
Type: c.OSCommand.FileType(status.Name),
|
||||
Type: self.getFileType(status.Name),
|
||||
ShortStatus: change,
|
||||
}
|
||||
files = append(files, file)
|
||||
@@ -74,13 +92,13 @@ type FileStatus struct {
|
||||
PreviousName string
|
||||
}
|
||||
|
||||
func (c *GitCommand) GitStatus(opts GitStatusOptions) ([]FileStatus, error) {
|
||||
func (c *FileLoader) GitStatus(opts GitStatusOptions) ([]FileStatus, error) {
|
||||
noRenamesFlag := ""
|
||||
if opts.NoRenames {
|
||||
noRenamesFlag = "--no-renames"
|
||||
noRenamesFlag = " --no-renames"
|
||||
}
|
||||
|
||||
statusLines, err := c.RunCommandWithOutput("git status %s --porcelain -z %s", opts.UntrackedFilesArg, noRenamesFlag)
|
||||
statusLines, err := c.cmd.New(fmt.Sprintf("git status %s --porcelain -z%s", opts.UntrackedFilesArg, noRenamesFlag)).RunWithOutput()
|
||||
if err != nil {
|
||||
return []FileStatus{}, err
|
||||
}
|
||||
@@ -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,4 +1,4 @@
|
||||
package commands
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -7,21 +7,34 @@ import (
|
||||
|
||||
"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)
|
||||
|
||||
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 log -g --abbrev=20 --format="%%h %%ct %%gs" %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) {
|
||||
err := cmdObj.RunAndProcessLines(func(line string) (bool, error) {
|
||||
fields := strings.SplitN(line, " ", 3)
|
||||
if len(fields) <= 2 {
|
||||
return false, nil
|
||||
@@ -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 {
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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(""))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,33 @@
|
||||
package commands
|
||||
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"
|
||||
)
|
||||
|
||||
func (c *GitCommand) GetTags() ([]*models.Tag, error) {
|
||||
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 := c.OSCommand.RunCommandWithOutput(`git tag --list --sort=-creatordate`)
|
||||
remoteBranchesStr, err := self.cmd.New(`git tag --list --sort=-creatordate`).RunWithOutput()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -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(), "auto"),
|
||||
}
|
||||
}
|
||||
|
||||
// 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,227 +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(
|
||||
"printf",
|
||||
`MM file1.txt\0A file3.txt\0AM file2.txt\0?? file4.txt\0UU 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)
|
||||
},
|
||||
},
|
||||
{
|
||||
"File with new line char",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return secureexec.Command(
|
||||
"printf",
|
||||
`MM a\nb.txt`,
|
||||
)
|
||||
},
|
||||
func(files []*models.File) {
|
||||
assert.Len(t, files, 1)
|
||||
|
||||
expected := []*models.File{
|
||||
{
|
||||
Name: "a\nb.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Added: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "MM a\nb.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "MM",
|
||||
},
|
||||
}
|
||||
|
||||
assert.EqualValues(t, expected, files)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Renamed files",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return secureexec.Command(
|
||||
"printf",
|
||||
`R after1.txt\0before1.txt\0RM after2.txt\0before2.txt`,
|
||||
)
|
||||
},
|
||||
func(files []*models.File) {
|
||||
assert.Len(t, files, 2)
|
||||
|
||||
expected := []*models.File{
|
||||
{
|
||||
Name: "after1.txt",
|
||||
PreviousName: "before1.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: false,
|
||||
Tracked: true,
|
||||
Added: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "R before1.txt -> after1.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "R ",
|
||||
},
|
||||
{
|
||||
Name: "after2.txt",
|
||||
PreviousName: "before2.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Added: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "RM before2.txt -> after2.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "RM",
|
||||
},
|
||||
}
|
||||
|
||||
assert.EqualValues(t, expected, files)
|
||||
},
|
||||
},
|
||||
{
|
||||
"File with arrow in name",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return secureexec.Command(
|
||||
"printf",
|
||||
`?? a -> b.txt`,
|
||||
)
|
||||
},
|
||||
func(files []*models.File) {
|
||||
assert.Len(t, files, 1)
|
||||
|
||||
expected := []*models.File{
|
||||
{
|
||||
Name: "a -> b.txt",
|
||||
HasStagedChanges: false,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "?? a -> b.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "??",
|
||||
},
|
||||
}
|
||||
|
||||
assert.EqualValues(t, expected, files)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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(""))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5,17 +5,33 @@ import (
|
||||
)
|
||||
|
||||
// A command object is a general way to represent a command to be run on the
|
||||
// command line. If you want to log the command you'll use .ToString() and
|
||||
// if you want to run it you'll use .GetCmd()
|
||||
// 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 {
|
||||
@@ -31,3 +47,25 @@ func (self *CmdObj) AddEnvVars(vars ...string) ICmdObj {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -4,95 +4,34 @@
|
||||
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, cmdObj ICmdObj, output func(string) string) 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()
|
||||
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
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,8 +3,61 @@
|
||||
|
||||
package oscommands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os/exec"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Buffer struct {
|
||||
b bytes.Buffer
|
||||
m sync.Mutex
|
||||
}
|
||||
|
||||
func (b *Buffer) Read(p []byte) (n int, err error) {
|
||||
b.m.Lock()
|
||||
defer b.m.Unlock()
|
||||
return b.b.Read(p)
|
||||
}
|
||||
func (b *Buffer) Write(p []byte) (n int, err error) {
|
||||
b.m.Lock()
|
||||
defer b.m.Unlock()
|
||||
return b.b.Write(p)
|
||||
}
|
||||
|
||||
// RunCommandWithOutputLiveWrapper runs a command live but because of windows compatibility this command can't be ran there
|
||||
// TODO: Remove this hack and replace it with a proper way to run commands live on windows
|
||||
func RunCommandWithOutputLiveWrapper(c *OSCommand, cmdObj ICmdObj, output func(string) string) error {
|
||||
return c.RunCommand(cmdObj.ToString())
|
||||
// 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
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+45
-282
@@ -1,43 +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
|
||||
Shell string
|
||||
ShellArg 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)
|
||||
@@ -46,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
|
||||
@@ -79,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 {
|
||||
@@ -102,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) {
|
||||
@@ -121,153 +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 = strings.NewReplacer(
|
||||
"^", "^^",
|
||||
"&", "^&",
|
||||
"|", "^|",
|
||||
"<", "^<",
|
||||
">", "^>",
|
||||
"%", "^%",
|
||||
).Replace(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(cmdObj ICmdObj, output func(string) string) error {
|
||||
return RunCommandWithOutputLiveWrapper(c, cmdObj, output)
|
||||
}
|
||||
|
||||
// DetectUnamePass detect a username / password / passphrase question in a command
|
||||
// promptUserForCredential is a function that gets executed when this function detect you need to fillin a password or passphrase
|
||||
// The promptUserForCredential argument will be "username", "password" or "passphrase" and expects the user's password/passphrase or username back
|
||||
func (c *OSCommand) DetectUnamePass(cmdObj ICmdObj, promptUserForCredential func(string) string) error {
|
||||
ttyText := ""
|
||||
errMessage := c.RunCommandWithOutputLive(cmdObj, 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.ShellCommandFromString(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"
|
||||
@@ -278,63 +138,36 @@ 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.RunShellCommand(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.RunShellCommand(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 {
|
||||
return c.Cmd.Quote(message)
|
||||
}
|
||||
|
||||
func (self *CmdObjBuilder) Quote(message string) string {
|
||||
var quote string
|
||||
if c.Platform.OS == "windows" {
|
||||
if self.platform.OS == "windows" {
|
||||
quote = `\"`
|
||||
message = strings.NewReplacer(
|
||||
`"`, `"'"'"`,
|
||||
@@ -423,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
|
||||
@@ -459,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)
|
||||
|
||||
@@ -522,35 +337,6 @@ 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 {
|
||||
escaped := strings.Replace(str, "\n", "\\n", -1)
|
||||
truncated := utils.TruncateWithEllipsis(escaped, 40)
|
||||
@@ -564,29 +350,6 @@ func (c *OSCommand) RemoveFile(path string) error {
|
||||
return c.removeFile(path)
|
||||
}
|
||||
|
||||
func (c *OSCommand) NewCmdObjFromStr(cmdStr string) ICmdObj {
|
||||
args := str.ToArgv(cmdStr)
|
||||
cmd := c.Command(args[0], args[1:]...)
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
return &CmdObj{
|
||||
cmdStr: cmdStr,
|
||||
cmd: cmd,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *OSCommand) NewCmdObjFromArgs(args []string) ICmdObj {
|
||||
cmd := c.Command(args[0], args[1:]...)
|
||||
|
||||
return &CmdObj{
|
||||
cmdStr: strings.Join(args, " "),
|
||||
cmd: cmd,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *OSCommand) NewCmdObj(cmd *exec.Cmd) ICmdObj {
|
||||
return &CmdObj{
|
||||
cmdStr: strings.Join(cmd.Args, " "),
|
||||
cmd: cmd,
|
||||
}
|
||||
func GetTempDir() string {
|
||||
return filepath.Join(os.TempDir(), "lazygit")
|
||||
}
|
||||
|
||||
@@ -7,59 +7,50 @@ import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestOSCommandOpenFileDarwin is a function.
|
||||
func TestOSCommandOpenFileDarwin(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
runner *FakeCmdObjRunner
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
return secureexec.Command("exit", "1")
|
||||
},
|
||||
func(err error) {
|
||||
filename: "test",
|
||||
runner: NewFakeRunner(t).
|
||||
ExpectArgs([]string{"bash", "-c", `open "test"`}, "", errors.New("error")),
|
||||
test: func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "bash", name)
|
||||
assert.Equal(t, []string{"-c", `open "test"`}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
filename: "test",
|
||||
runner: NewFakeRunner(t).
|
||||
ExpectArgs([]string{"bash", "-c", `open "test"`}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"filename with spaces",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "bash", name)
|
||||
assert.Equal(t, []string{"-c", `open "filename with spaces"`}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
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 := NewDummyOSCommand()
|
||||
OSCmd.Platform.OS = "darwin"
|
||||
OSCmd.Command = s.command
|
||||
OSCmd.Config.GetUserConfig().OS.OpenCommand = "open {{filename}}"
|
||||
oSCmd := NewDummyOSCommandWithRunner(s.runner)
|
||||
oSCmd.Platform.OS = "darwin"
|
||||
oSCmd.UserConfig.OS.OpenCommand = "open {{filename}}"
|
||||
|
||||
s.test(OSCmd.OpenFile(s.filename))
|
||||
s.test(oSCmd.OpenFile(s.filename))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,72 +58,125 @@ func TestOSCommandOpenFileDarwin(t *testing.T) {
|
||||
func TestOSCommandOpenFileLinux(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
runner *FakeCmdObjRunner
|
||||
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) {
|
||||
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)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "bash", name)
|
||||
assert.Equal(t, []string{"-c", `xdg-open "test" > /dev/null`}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
filename: "test",
|
||||
runner: NewFakeRunner(t).
|
||||
ExpectArgs([]string{"bash", "-c", `xdg-open "test" > /dev/null`}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"filename with spaces",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "bash", name)
|
||||
assert.Equal(t, []string{"-c", `xdg-open "filename with spaces" > /dev/null`}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
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)
|
||||
},
|
||||
},
|
||||
{
|
||||
"let's_test_with_single_quote",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "bash", name)
|
||||
assert.Equal(t, []string{"-c", `xdg-open "let's_test_with_single_quote" > /dev/null`}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
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)
|
||||
},
|
||||
},
|
||||
{
|
||||
"$USER.txt",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "bash", name)
|
||||
assert.Equal(t, []string{"-c", `xdg-open "\$USER.txt" > /dev/null`}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
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 := NewDummyOSCommand()
|
||||
OSCmd.Command = s.command
|
||||
OSCmd.Platform.OS = "linux"
|
||||
OSCmd.Config.GetUserConfig().OS.OpenCommand = `xdg-open {{filename}} > /dev/null`
|
||||
oSCmd := NewDummyOSCommandWithRunner(s.runner)
|
||||
oSCmd.Platform.OS = "linux"
|
||||
oSCmd.UserConfig.OS.OpenCommand = `xdg-open {{filename}} > /dev/null`
|
||||
|
||||
s.test(OSCmd.OpenFile(s.filename))
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,7 @@ import (
|
||||
"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)
|
||||
@@ -32,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)
|
||||
@@ -53,11 +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())
|
||||
}
|
||||
}
|
||||
|
||||
// TestOSCommandQuote is a function.
|
||||
func TestOSCommandQuote(t *testing.T) {
|
||||
osCommand := NewDummyOSCommand()
|
||||
|
||||
@@ -109,7 +108,6 @@ func TestOSCommandQuoteWindows(t *testing.T) {
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
|
||||
// TestOSCommandFileType is a function.
|
||||
func TestOSCommandFileType(t *testing.T) {
|
||||
type scenario struct {
|
||||
path string
|
||||
@@ -162,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,76 +11,68 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestOSCommandOpenFileWindows tests the OpenFile command on Linux
|
||||
func TestOSCommandOpenFileWindows(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
runner *FakeCmdObjRunner
|
||||
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) {
|
||||
filename: "test",
|
||||
runner: NewFakeRunner(t).
|
||||
ExpectArgs([]string{"cmd", "/c", "start", "", "test"}, "", errors.New("error")),
|
||||
test: func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "cmd", name)
|
||||
assert.Equal(t, []string{"/c", "start", "", "test"}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
filename: "test",
|
||||
runner: NewFakeRunner(t).
|
||||
ExpectArgs([]string{"cmd", "/c", "start", "", "test"}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"filename with spaces",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "cmd", name)
|
||||
assert.Equal(t, []string{"/c", "start", "", "filename with spaces"}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
filename: "filename with spaces",
|
||||
runner: NewFakeRunner(t).
|
||||
ExpectArgs([]string{"cmd", "/c", "start", "", "filename with spaces"}, "", nil),
|
||||
test: func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"let's_test_with_single_quote",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "cmd", name)
|
||||
assert.Equal(t, []string{"/c", "start", "", "let's_test_with_single_quote"}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
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)
|
||||
},
|
||||
},
|
||||
{
|
||||
"$USER.txt",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "cmd", name)
|
||||
assert.Equal(t, []string{"/c", "start", "", "$USER.txt"}, arg)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
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 := NewDummyOSCommand()
|
||||
OSCmd.Command = s.command
|
||||
OSCmd.Platform.OS = "windows"
|
||||
OSCmd.Config.GetUserConfig().OS.OpenCommand = `start "" {{filename}}`
|
||||
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))
|
||||
s.test(oSCmd.OpenFile(s.filename))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,186 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
)
|
||||
|
||||
// Service is a service that repository is on (Github, Bitbucket, ...)
|
||||
type Service struct {
|
||||
Name string
|
||||
pullRequestURLIntoDefaultBranch func(owner string, repository string, from string) string
|
||||
pullRequestURLIntoTargetBranch func(owner string, repository string, from string, to string) string
|
||||
}
|
||||
|
||||
// NewService builds a Service based on the host type
|
||||
func NewService(typeName string, repositoryDomain string, siteDomain string) *Service {
|
||||
var service *Service
|
||||
|
||||
switch typeName {
|
||||
case "github":
|
||||
service = &Service{
|
||||
Name: repositoryDomain,
|
||||
pullRequestURLIntoDefaultBranch: func(owner string, repository string, from string) string {
|
||||
return fmt.Sprintf("https://%s/%s/%s/compare/%s?expand=1", siteDomain, owner, repository, from)
|
||||
},
|
||||
pullRequestURLIntoTargetBranch: func(owner string, repository string, from string, to string) string {
|
||||
return fmt.Sprintf("https://%s/%s/%s/compare/%s...%s?expand=1", siteDomain, owner, repository, to, from)
|
||||
},
|
||||
}
|
||||
case "bitbucket":
|
||||
service = &Service{
|
||||
Name: repositoryDomain,
|
||||
pullRequestURLIntoDefaultBranch: func(owner string, repository string, from string) string {
|
||||
return fmt.Sprintf("https://%s/%s/%s/pull-requests/new?source=%s&t=1", siteDomain, owner, repository, from)
|
||||
},
|
||||
pullRequestURLIntoTargetBranch: func(owner string, repository string, from string, to string) string {
|
||||
return fmt.Sprintf("https://%s/%s/%s/pull-requests/new?source=%s&dest=%s&t=1", siteDomain, owner, repository, from, to)
|
||||
},
|
||||
}
|
||||
case "gitlab":
|
||||
service = &Service{
|
||||
Name: repositoryDomain,
|
||||
pullRequestURLIntoDefaultBranch: func(owner string, repository string, from string) string {
|
||||
return fmt.Sprintf("https://%s/%s/%s/merge_requests/new?merge_request[source_branch]=%s", siteDomain, owner, repository, from)
|
||||
},
|
||||
pullRequestURLIntoTargetBranch: func(owner string, repository string, from string, to string) string {
|
||||
return fmt.Sprintf("https://%s/%s/%s/merge_requests/new?merge_request[source_branch]=%s&merge_request[target_branch]=%s", siteDomain, owner, repository, from, to)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
func (s *Service) PullRequestURL(owner string, repository string, from string, to string) string {
|
||||
if to == "" {
|
||||
return s.pullRequestURLIntoDefaultBranch(owner, repository, from)
|
||||
} else {
|
||||
return s.pullRequestURLIntoTargetBranch(owner, repository, from, to)
|
||||
}
|
||||
}
|
||||
|
||||
// PullRequest opens a link in browser to create new pull request
|
||||
// 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
|
||||
}
|
||||
|
||||
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(from string, to string) (string, error) {
|
||||
pullRequestURL, err := pr.getPullRequestURL(from, to)
|
||||
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(from string, to string) (string, error) {
|
||||
pullRequestURL, err := pr.getPullRequestURL(from, to)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return pullRequestURL, pr.GitCommand.OSCommand.CopyToClipboard(pullRequestURL)
|
||||
}
|
||||
|
||||
func (pr *PullRequest) getPullRequestURL(from string, to string) (string, error) {
|
||||
branchExistsOnRemote := pr.GitCommand.CheckRemoteBranchExists(from)
|
||||
|
||||
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 := gitService.PullRequestURL(repoInfo.Owner, repoInfo.Repository, from, to)
|
||||
|
||||
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,256 +0,0 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestCreatePullRequest is a function.
|
||||
func TestCreatePullRequest(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
from string
|
||||
to string
|
||||
remoteUrl string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(url string, err error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket",
|
||||
from: "feature/profile-page",
|
||||
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "bash")
|
||||
assert.Equal(t, args, []string{"-c", `open "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1"`})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket with http remote url",
|
||||
from: "feature/events",
|
||||
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "bash")
|
||||
assert.Equal(t, args, []string{"-c", `open "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1"`})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on github",
|
||||
from: "feature/sum-operation",
|
||||
remoteUrl: "git@github.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@github.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "bash")
|
||||
assert.Equal(t, args, []string{"-c", `open "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1"`})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket with specific target branch",
|
||||
from: "feature/profile-page/avatar",
|
||||
to: "feature/profile-page",
|
||||
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "bash")
|
||||
assert.Equal(t, args, []string{"-c", `open "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar&dest=feature/profile-page&t=1"`})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar&dest=feature/profile-page&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket with http remote url with specified target branch",
|
||||
from: "feature/remote-events",
|
||||
to: "feature/events",
|
||||
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "bash")
|
||||
assert.Equal(t, args, []string{"-c", `open "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events&dest=feature/events&t=1"`})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events&dest=feature/events&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on github with specific target branch",
|
||||
from: "feature/sum-operation",
|
||||
to: "feature/operations",
|
||||
remoteUrl: "git@github.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@github.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "bash")
|
||||
assert.Equal(t, args, []string{"-c", `open "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1"`})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab",
|
||||
from: "feature/ui",
|
||||
remoteUrl: "git@gitlab.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "bash")
|
||||
assert.Equal(t, args, []string{"-c", `open "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"`})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab in nested groups",
|
||||
from: "feature/ui",
|
||||
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "bash")
|
||||
assert.Equal(t, args, []string{"-c", `open "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"`})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab with specific target branch",
|
||||
from: "feature/commit-ui",
|
||||
to: "epic/ui",
|
||||
remoteUrl: "git@gitlab.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "bash")
|
||||
assert.Equal(t, args, []string{"-c", `open "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui"`})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab with specific target branch in nested groups",
|
||||
from: "feature/commit-ui",
|
||||
to: "epic/ui",
|
||||
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "bash")
|
||||
assert.Equal(t, args, []string{"-c", `open "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui"`})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Throws an error if git service is unsupported",
|
||||
from: "feature/divide-operation",
|
||||
remoteUrl: "git@something.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCommand := NewDummyGitCommand()
|
||||
gitCommand.OSCommand.Command = s.command
|
||||
gitCommand.OSCommand.Platform.OS = "darwin"
|
||||
gitCommand.OSCommand.Platform.Shell = "bash"
|
||||
gitCommand.OSCommand.Platform.ShellArg = "-c"
|
||||
gitCommand.OSCommand.Config.GetUserConfig().OS.OpenLinkCommand = "open {{link}}"
|
||||
gitCommand.OSCommand.Config.GetUserConfig().Services = map[string]string{
|
||||
// valid configuration for a custom service URL
|
||||
"git.work.com": "gitlab:code.work.com",
|
||||
// invalid configurations for a custom service URL
|
||||
"invalid.work.com": "noservice:invalid.work.com",
|
||||
"noservice.work.com": "noservice.work.com",
|
||||
}
|
||||
gitCommand.GitConfig = git_config.NewFakeGitConfig(map[string]string{"remote.origin.url": s.remoteUrl})
|
||||
dummyPullRequest := NewPullRequest(gitCommand)
|
||||
s.test(dummyPullRequest.Create(s.from, s.to))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGetRepoInfoFromURL is a function.
|
||||
func TestGetRepoInfoFromURL(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
repoURL string
|
||||
test func(*RepoInformation)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Returns repository information for git remote url",
|
||||
"git@github.com:petersmith/super_calculator",
|
||||
func(repoInfo *RepoInformation) {
|
||||
assert.EqualValues(t, repoInfo.Owner, "petersmith")
|
||||
assert.EqualValues(t, repoInfo.Repository, "super_calculator")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Returns repository information for http remote url",
|
||||
"https://my_username@bitbucket.org/johndoe/social_network.git",
|
||||
func(repoInfo *RepoInformation) {
|
||||
assert.EqualValues(t, repoInfo.Owner, "johndoe")
|
||||
assert.EqualValues(t, repoInfo.Repository, "social_network")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
s.test(getRepoInfoFromURL(s.repoURL))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestCreatePullRequestOnWindows is a function.
|
||||
func TestCreatePullRequestOnWindows(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
from string
|
||||
to string
|
||||
remoteUrl string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(url string, err error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket",
|
||||
from: "feature/profile-page",
|
||||
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "cmd")
|
||||
assert.Equal(t, args, []string{"/c", "start", "", "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page^&t=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket with http remote url",
|
||||
from: "feature/events",
|
||||
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "cmd")
|
||||
assert.Equal(t, args, []string{"/c", "start", "", "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events^&t=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on github",
|
||||
from: "feature/sum-operation",
|
||||
remoteUrl: "git@github.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@github.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "cmd")
|
||||
assert.Equal(t, args, []string{"/c", "start", "", "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/sum-operation?expand=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket with specific target branch",
|
||||
from: "feature/profile-page/avatar",
|
||||
to: "feature/profile-page",
|
||||
remoteUrl: "git@bitbucket.org:johndoe/social_network.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "cmd")
|
||||
assert.Equal(t, args, []string{"/c", "start", "", "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar^&dest=feature/profile-page^&t=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page/avatar&dest=feature/profile-page&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on bitbucket with http remote url with specified target branch",
|
||||
from: "feature/remote-events",
|
||||
to: "feature/events",
|
||||
remoteUrl: "https://my_username@bitbucket.org/johndoe/social_network.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "cmd")
|
||||
assert.Equal(t, args, []string{"/c", "start", "", "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events^&dest=feature/events^&t=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/remote-events&dest=feature/events&t=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on github with specific target branch",
|
||||
from: "feature/sum-operation",
|
||||
to: "feature/operations",
|
||||
remoteUrl: "git@github.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@github.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "cmd")
|
||||
assert.Equal(t, args, []string{"/c", "start", "", "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://github.com/peter/calculator/compare/feature/operations...feature/sum-operation?expand=1", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab",
|
||||
from: "feature/ui",
|
||||
remoteUrl: "git@gitlab.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "cmd")
|
||||
assert.Equal(t, args, []string{"/c", "start", "", "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab in nested groups",
|
||||
from: "feature/ui",
|
||||
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "cmd")
|
||||
assert.Equal(t, args, []string{"/c", "start", "", "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab with specific target branch",
|
||||
from: "feature/commit-ui",
|
||||
to: "epic/ui",
|
||||
remoteUrl: "git@gitlab.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "cmd")
|
||||
assert.Equal(t, args, []string{"/c", "start", "", "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui^&merge_request[target_branch]=epic/ui"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Opens a link to new pull request on gitlab with specific target branch in nested groups",
|
||||
from: "feature/commit-ui",
|
||||
to: "epic/ui",
|
||||
remoteUrl: "git@gitlab.com:peter/public/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return secureexec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "cmd")
|
||||
assert.Equal(t, args, []string{"/c", "start", "", "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui^&merge_request[target_branch]=epic/ui"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://gitlab.com/peter/public/calculator/merge_requests/new?merge_request[source_branch]=feature/commit-ui&merge_request[target_branch]=epic/ui", url)
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "Throws an error if git service is unsupported",
|
||||
from: "feature/divide-operation",
|
||||
remoteUrl: "git@something.com:peter/calculator.git",
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(url string, err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCommand := NewDummyGitCommand()
|
||||
gitCommand.OSCommand.Command = s.command
|
||||
gitCommand.OSCommand.Platform.OS = "windows"
|
||||
gitCommand.OSCommand.Platform.Shell = "cmd"
|
||||
gitCommand.OSCommand.Platform.ShellArg = "/c"
|
||||
gitCommand.OSCommand.Config.GetUserConfig().OS.OpenLinkCommand = `start "" {{link}}`
|
||||
gitCommand.OSCommand.Config.GetUserConfig().Services = map[string]string{
|
||||
// valid configuration for a custom service URL
|
||||
"git.work.com": "gitlab:code.work.com",
|
||||
// invalid configurations for a custom service URL
|
||||
"invalid.work.com": "noservice:invalid.work.com",
|
||||
"noservice.work.com": "noservice.work.com",
|
||||
}
|
||||
gitCommand.GitConfig = git_config.NewFakeGitConfig(map[string]string{"remote.origin.url": s.remoteUrl})
|
||||
dummyPullRequest := NewPullRequest(gitCommand)
|
||||
s.test(dummyPullRequest.Create(s.from, s.to))
|
||||
})
|
||||
}
|
||||
}
|
||||
+25
-34
@@ -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) {
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
+28
-11
@@ -2,36 +2,53 @@ package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
)
|
||||
|
||||
func (c *GitCommand) AddRemote(name string, url string) error {
|
||||
return c.RunCommand("git remote add %s %s", c.OSCommand.Quote(name), c.OSCommand.Quote(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", c.OSCommand.Quote(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", c.OSCommand.Quote(oldRemoteName), c.OSCommand.Quote(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", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(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", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(branchName))
|
||||
cmdObj := c.NewCmdObjFromStr(command)
|
||||
return c.OSCommand.DetectUnamePass(cmdObj, 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(branchName string) bool {
|
||||
_, err := c.OSCommand.RunCommandWithOutput(
|
||||
"git show-ref --verify -- refs/remotes/origin/%s",
|
||||
c.OSCommand.Quote(branchName),
|
||||
)
|
||||
_, err := c.Cmd.
|
||||
New(
|
||||
fmt.Sprintf("git show-ref --verify -- refs/remotes/origin/%s",
|
||||
c.Cmd.Quote(branchName),
|
||||
)).
|
||||
RunWithOutput()
|
||||
|
||||
return err == nil
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+11
-17
@@ -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
|
||||
|
||||
+61
-40
@@ -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", c.OSCommand.Quote(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", c.OSCommand.Quote(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", c.OSCommand.Quote(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", c.OSCommand.Quote(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", c.OSCommand.Quote(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", c.OSCommand.Quote(name), c.OSCommand.Quote(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", c.OSCommand.Quote(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", c.OSCommand.Quote(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", c.OSCommand.Quote(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()
|
||||
}
|
||||
|
||||
+26
-17
@@ -4,18 +4,18 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
)
|
||||
|
||||
// Push pushes to a branch
|
||||
type PushOpts struct {
|
||||
Force bool
|
||||
UpstreamRemote string
|
||||
UpstreamBranch string
|
||||
SetUpstream bool
|
||||
PromptUserForCredential func(string) string
|
||||
Force bool
|
||||
UpstreamRemote string
|
||||
UpstreamBranch string
|
||||
SetUpstream bool
|
||||
}
|
||||
|
||||
func (c *GitCommand) Push(opts PushOpts) error {
|
||||
func (c *GitCommand) PushCmdObj(opts PushOpts) (oscommands.ICmdObj, error) {
|
||||
cmdStr := "git push"
|
||||
|
||||
if opts.Force {
|
||||
@@ -32,13 +32,22 @@ func (c *GitCommand) Push(opts PushOpts) error {
|
||||
|
||||
if opts.UpstreamBranch != "" {
|
||||
if opts.UpstreamRemote == "" {
|
||||
return errors.New(c.Tr.MustSpecifyOriginError)
|
||||
return nil, errors.New(c.Tr.MustSpecifyOriginError)
|
||||
}
|
||||
cmdStr += " " + c.OSCommand.Quote(opts.UpstreamBranch)
|
||||
}
|
||||
|
||||
cmdObj := c.NewCmdObjFromStr(cmdStr)
|
||||
return c.OSCommand.DetectUnamePass(cmdObj, opts.PromptUserForCredential)
|
||||
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 {
|
||||
@@ -58,8 +67,8 @@ func (c *GitCommand) Fetch(opts FetchOptions) error {
|
||||
cmdStr = fmt.Sprintf("%s %s", cmdStr, c.OSCommand.Quote(opts.BranchName))
|
||||
}
|
||||
|
||||
cmdObj := c.NewCmdObjFromStr(cmdStr)
|
||||
return c.OSCommand.DetectUnamePass(cmdObj, 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)
|
||||
}
|
||||
@@ -94,18 +103,18 @@ func (c *GitCommand) Pull(opts PullOptions) error {
|
||||
|
||||
// setting GIT_SEQUENCE_EDITOR to ':' as a way of skipping it, in case the user
|
||||
// has 'pull.rebase = interactive' configured.
|
||||
cmdObj := c.NewCmdObjFromStr(cmdStr).AddEnvVars("GIT_SEQUENCE_EDITOR=:")
|
||||
return c.OSCommand.DetectUnamePass(cmdObj, opts.PromptUserForCredential)
|
||||
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 {
|
||||
cmdStr := fmt.Sprintf("git fetch %s %s:%s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(remoteBranchName), c.OSCommand.Quote(branchName))
|
||||
cmdObj := c.NewCmdObjFromStr(cmdStr)
|
||||
return c.OSCommand.DetectUnamePass(cmdObj, promptUserForCredential)
|
||||
cmdObj := c.Cmd.New(cmdStr)
|
||||
return c.DetectUnamePass(cmdObj, promptUserForCredential)
|
||||
}
|
||||
|
||||
func (c *GitCommand) FetchRemote(remoteName string, promptUserForCredential func(string) string) error {
|
||||
cmdStr := fmt.Sprintf("git fetch %s", c.OSCommand.Quote(remoteName))
|
||||
cmdObj := c.NewCmdObjFromStr(cmdStr)
|
||||
return c.OSCommand.DetectUnamePass(cmdObj, promptUserForCredential)
|
||||
cmdObj := c.Cmd.New(cmdStr)
|
||||
return c.DetectUnamePass(cmdObj, promptUserForCredential)
|
||||
}
|
||||
|
||||
+42
-113
@@ -1,164 +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
|
||||
command func(string, ...string) *exec.Cmd
|
||||
opts PushOpts
|
||||
test func(error)
|
||||
}
|
||||
|
||||
prompt := func(passOrUname string) string {
|
||||
return "\n"
|
||||
test func(oscommands.ICmdObj, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Push with force disabled",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
PushOpts{Force: false, PromptUserForCredential: prompt},
|
||||
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",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push", "--force-with-lease"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
PushOpts{Force: true, PromptUserForCredential: prompt},
|
||||
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 an error occurring",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push"}, args)
|
||||
return secureexec.Command("test")
|
||||
testName: "Push with force disabled, upstream supplied",
|
||||
opts: PushOpts{
|
||||
Force: false,
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "master",
|
||||
},
|
||||
PushOpts{Force: false, PromptUserForCredential: prompt},
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with force disabled, upstream supplied",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push", "origin", "master"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
PushOpts{
|
||||
Force: false,
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "master",
|
||||
PromptUserForCredential: prompt,
|
||||
},
|
||||
func(err error) {
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.ToString(), `git push "origin" "master"`)
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with force disabled, setting upstream",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push", "--set-upstream", "origin", "master"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
testName: "Push with force disabled, setting upstream",
|
||||
opts: PushOpts{
|
||||
Force: false,
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "master",
|
||||
SetUpstream: true,
|
||||
},
|
||||
PushOpts{
|
||||
Force: false,
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "master",
|
||||
PromptUserForCredential: prompt,
|
||||
SetUpstream: true,
|
||||
},
|
||||
func(err error) {
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
assert.Equal(t, cmdObj.ToString(), `git push --set-upstream "origin" "master"`)
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with force enabled, setting upstream",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push", "--force-with-lease", "--set-upstream", "origin", "master"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
testName: "Push with force enabled, setting upstream",
|
||||
opts: PushOpts{
|
||||
Force: true,
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "master",
|
||||
SetUpstream: true,
|
||||
},
|
||||
PushOpts{
|
||||
Force: true,
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "master",
|
||||
PromptUserForCredential: prompt,
|
||||
SetUpstream: true,
|
||||
},
|
||||
func(err error) {
|
||||
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)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with remote branch but no origin",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return nil
|
||||
testName: "Push with remote branch but no origin",
|
||||
opts: PushOpts{
|
||||
Force: true,
|
||||
UpstreamRemote: "",
|
||||
UpstreamBranch: "master",
|
||||
SetUpstream: true,
|
||||
},
|
||||
PushOpts{
|
||||
Force: true,
|
||||
UpstreamRemote: "",
|
||||
UpstreamBranch: "master",
|
||||
PromptUserForCredential: prompt,
|
||||
SetUpstream: true,
|
||||
},
|
||||
func(err error) {
|
||||
test: func(cmdObj oscommands.ICmdObj, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.EqualValues(t, "Must specify a remote if specifying a branch", err.Error())
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with force disabled, upstream supplied",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push", "origin", "master"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
PushOpts{
|
||||
Force: false,
|
||||
UpstreamRemote: "origin",
|
||||
UpstreamBranch: "master",
|
||||
PromptUserForCredential: prompt,
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
err := gitCmd.Push(s.opts)
|
||||
s.test(err)
|
||||
gitCmd := NewDummyGitCommandWithRunner(oscommands.NewFakeRunner(t))
|
||||
s.test(gitCmd.PushCmdObj(s.opts))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,15 +5,19 @@ import (
|
||||
)
|
||||
|
||||
func (c *GitCommand) CreateLightweightTag(tagName string, commitSha string) error {
|
||||
return c.RunCommand("git tag -- %s %s", c.OSCommand.Quote(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", c.OSCommand.Quote(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 {
|
||||
cmdStr := fmt.Sprintf("git push %s %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(tagName))
|
||||
cmdObj := c.NewCmdObjFromStr(cmdStr)
|
||||
return c.OSCommand.DetectUnamePass(cmdObj, promptUserForCredential)
|
||||
cmdObj := c.Cmd.New(cmdStr)
|
||||
return c.DetectUnamePass(cmdObj, promptUserForCredential)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -31,21 +31,21 @@ type AppConfig struct {
|
||||
// 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
|
||||
GetTempDir() string
|
||||
ReloadUserConfig() error
|
||||
|
||||
GetAppState() *AppState
|
||||
SaveAppState() error
|
||||
SetIsNewRepo(bool)
|
||||
GetIsNewRepo() bool
|
||||
ReloadUserConfig() error
|
||||
ShowCommandLogOnStartup() bool
|
||||
}
|
||||
|
||||
// NewAppConfig makes a new app config
|
||||
@@ -167,37 +167,22 @@ func loadUserConfig(configFiles []string, base *UserConfig) (*UserConfig, error)
|
||||
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
|
||||
}
|
||||
@@ -226,10 +211,6 @@ func (c *AppConfig) GetUserConfigDir() string {
|
||||
return c.UserConfigDir
|
||||
}
|
||||
|
||||
func (c *AppConfig) GetTempDir() string {
|
||||
return c.TempDir
|
||||
}
|
||||
|
||||
func (c *AppConfig) ReloadUserConfig() error {
|
||||
userConfig, err := loadUserConfigWithDefaults(c.UserConfigPaths)
|
||||
if err != nil {
|
||||
@@ -277,17 +258,6 @@ func (c *AppConfig) SaveAppState() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// originally we could only hide the command log permanently via the config
|
||||
// but now we do it via state. So we need to still support the config for the
|
||||
// sake of backwards compatibility
|
||||
func (c *AppConfig) ShowCommandLogOnStartup() bool {
|
||||
if !c.UserConfig.Gui.ShowCommandLog {
|
||||
return false
|
||||
}
|
||||
|
||||
return !c.AppState.HideCommandLog
|
||||
}
|
||||
|
||||
// loadAppState loads recorded AppState from file
|
||||
func loadAppState() (*AppState, error) {
|
||||
filepath, err := configFilePath("state.yml")
|
||||
|
||||
@@ -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"`
|
||||
@@ -60,6 +61,7 @@ type CommitLengthConfig struct {
|
||||
|
||||
type GitConfig struct {
|
||||
Paging PagingConfig `yaml:"paging"`
|
||||
Commit CommitConfig `yaml:"commit"`
|
||||
Merging MergingConfig `yaml:"merging"`
|
||||
SkipHookPrefix string `yaml:"skipHookPrefix"`
|
||||
AutoFetch bool `yaml:"autoFetch"`
|
||||
@@ -68,7 +70,10 @@ type GitConfig struct {
|
||||
OverrideGpg bool `yaml:"overrideGpg"`
|
||||
DisableForcePushing bool `yaml:"disableForcePushing"`
|
||||
CommitPrefixes map[string]CommitPrefixConfig `yaml:"commitPrefixes"`
|
||||
ParseEmoji bool `yaml:"parseEmoji"`
|
||||
// this shoudl really be under 'gui', not 'git'
|
||||
ParseEmoji bool `yaml:"parseEmoji"`
|
||||
Log LogConfig `yaml:"log"`
|
||||
DiffContextSize int `yaml:"diffContextSize"`
|
||||
}
|
||||
|
||||
type PagingConfig struct {
|
||||
@@ -77,11 +82,20 @@ 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 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 {
|
||||
Pattern string `yaml:"pattern"`
|
||||
Replace string `yaml:"replace"`
|
||||
@@ -117,6 +131,8 @@ type KeybindingUniversalConfig struct {
|
||||
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"`
|
||||
@@ -166,6 +182,8 @@ type KeybindingUniversalConfig struct {
|
||||
AppendNewline string `yaml:"appendNewline"`
|
||||
ExtrasMenu string `yaml:"extrasMenu"`
|
||||
ToggleWhitespaceInDiffView string `yaml:"toggleWhitespaceInDiffView"`
|
||||
IncreaseContextInDiffView string `yaml:"increaseContextInDiffView"`
|
||||
DecreaseContextInDiffView string `yaml:"decreaseContextInDiffView"`
|
||||
}
|
||||
|
||||
type KeybindingStatusConfig struct {
|
||||
@@ -227,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 {
|
||||
@@ -333,10 +353,17 @@ func GetDefaultConfig() *UserConfig {
|
||||
ColorArg: "always",
|
||||
Pager: "",
|
||||
UseConfig: false},
|
||||
Commit: CommitConfig{
|
||||
SignOff: false,
|
||||
},
|
||||
Merging: MergingConfig{
|
||||
ManualCommit: false,
|
||||
Args: "",
|
||||
},
|
||||
Log: LogConfig{
|
||||
Order: "topo-order",
|
||||
ShowGraph: "when-maximised",
|
||||
},
|
||||
SkipHookPrefix: "WIP",
|
||||
AutoFetch: true,
|
||||
BranchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --",
|
||||
@@ -344,6 +371,7 @@ func GetDefaultConfig() *UserConfig {
|
||||
DisableForcePushing: false,
|
||||
CommitPrefixes: map[string]CommitPrefixConfig(nil),
|
||||
ParseEmoji: false,
|
||||
DiffContextSize: 3,
|
||||
},
|
||||
Refresher: RefresherConfig{
|
||||
RefreshInterval: 10,
|
||||
@@ -370,6 +398,8 @@ func GetDefaultConfig() *UserConfig {
|
||||
NextItemAlt: "j",
|
||||
PrevPage: ",",
|
||||
NextPage: ".",
|
||||
ScrollLeft: "H",
|
||||
ScrollRight: "L",
|
||||
GotoTop: "<",
|
||||
GotoBottom: ">",
|
||||
PrevBlock: "<left>",
|
||||
@@ -419,6 +449,8 @@ func GetDefaultConfig() *UserConfig {
|
||||
AppendNewline: "<a-enter>",
|
||||
ExtrasMenu: "@",
|
||||
ToggleWhitespaceInDiffView: "<c-w>",
|
||||
IncreaseContextInDiffView: "}",
|
||||
DecreaseContextInDiffView: "{",
|
||||
},
|
||||
Status: KeybindingStatusConfig{
|
||||
CheckForUpdate: "u",
|
||||
@@ -476,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
|
||||
@@ -259,7 +259,7 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
|
||||
fullHeightBox("stash"),
|
||||
}
|
||||
} else if height >= 28 {
|
||||
accordionMode := gui.Config.GetUserConfig().Gui.ExpandFocusedSidePanel
|
||||
accordionMode := gui.UserConfig.Gui.ExpandFocusedSidePanel
|
||||
accordionBox := func(defaultBox *boxlayout.Box) *boxlayout.Box {
|
||||
if accordionMode && defaultBox.Window == currentWindow {
|
||||
return &boxlayout.Box{
|
||||
|
||||
+52
-30
@@ -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
|
||||
}
|
||||
|
||||
+19
-13
@@ -1,6 +1,7 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -25,17 +26,15 @@ func (gui *Gui) getSelectedBranch() *models.Branch {
|
||||
return gui.State.Branches[selectedLine]
|
||||
}
|
||||
|
||||
func (gui *Gui) handleBranchSelect() error {
|
||||
func (gui *Gui) branchesRenderToMain() error {
|
||||
var task updateTask
|
||||
branch := gui.getSelectedBranch()
|
||||
if branch == nil {
|
||||
task = NewRenderStringTask(gui.Tr.NoBranchesThisRepo)
|
||||
} else {
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.GetBranchGraphCmdStr(branch.Name),
|
||||
)
|
||||
cmdObj := gui.GitCommand.GetBranchGraphCmdObj(branch.Name)
|
||||
|
||||
task = NewRunPtyTask(cmd)
|
||||
task = NewRunPtyTask(cmdObj.GetCmd())
|
||||
}
|
||||
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
@@ -56,17 +55,13 @@ func (gui *Gui) refreshBranches() {
|
||||
// which allows us to order them correctly. So if we're filtering we'll just
|
||||
// manually load all the reflog commits here
|
||||
var err error
|
||||
reflogCommits, _, err = gui.GitCommand.GetReflogCommits(nil, "")
|
||||
reflogCommits, _, err = gui.GitCommand.Loaders.ReflogCommits.GetReflogCommits(nil, "")
|
||||
if err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
builder, err := commands.NewBranchListBuilder(gui.Log, gui.GitCommand, reflogCommits)
|
||||
if err != nil {
|
||||
_ = gui.surfaceError(err)
|
||||
}
|
||||
gui.State.Branches = builder.Build()
|
||||
gui.State.Branches = gui.GitCommand.Loaders.Branches.Load(reflogCommits)
|
||||
|
||||
if err := gui.postRefreshUpdate(gui.State.Contexts.Branches); err != nil {
|
||||
gui.Log.Error(err)
|
||||
@@ -104,13 +99,24 @@ func (gui *Gui) handleCreatePullRequestMenu() error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCopyPullRequestURLPress() error {
|
||||
pullRequest := commands.NewPullRequest(gui.GitCommand)
|
||||
hostingServiceMgr := gui.getHostingServiceMgr()
|
||||
|
||||
branch := gui.getSelectedBranch()
|
||||
url, err := pullRequest.CopyURL(branch.Name, "")
|
||||
|
||||
branchExistsOnRemote := gui.GitCommand.CheckRemoteBranchExists(branch.Name)
|
||||
|
||||
if !branchExistsOnRemote {
|
||||
return gui.surfaceError(errors.New(gui.Tr.NoBranchOnRemote))
|
||||
}
|
||||
|
||||
url, err := hostingServiceMgr.GetPullRequestURL(branch.Name, "")
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
if err := gui.GitCommand.OSCommand.CopyToClipboard(url); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(fmt.Sprintf("Copying to clipboard: '%s'", url), "Copy URL", false))
|
||||
|
||||
gui.raiseToast(gui.Tr.PullRequestURLCopiedToClipboard)
|
||||
|
||||
@@ -33,7 +33,7 @@ func (gui *Gui) handleCopyCommit() error {
|
||||
return err
|
||||
}
|
||||
|
||||
item, ok := context.SelectedItem()
|
||||
item, ok := context.GetSelectedItem()
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -41,11 +41,11 @@ func (gui *Gui) GetOnRunCommand() func(entry oscommands.CmdLogEntry) {
|
||||
func (gui *Gui) printCommandLogHeader() {
|
||||
introStr := fmt.Sprintf(
|
||||
gui.Tr.CommandLogHeader,
|
||||
gui.getKeyDisplay(gui.Config.GetUserConfig().Keybinding.Universal.ExtrasMenu),
|
||||
gui.getKeyDisplay(gui.UserConfig.Keybinding.Universal.ExtrasMenu),
|
||||
)
|
||||
fmt.Fprintln(gui.Views.Extras, style.FgCyan.Sprint(introStr))
|
||||
|
||||
if gui.Config.GetUserConfig().Gui.ShowRandomTip {
|
||||
if gui.UserConfig.Gui.ShowRandomTip {
|
||||
fmt.Fprintf(
|
||||
gui.Views.Extras,
|
||||
"%s: %s",
|
||||
@@ -56,7 +56,7 @@ func (gui *Gui) printCommandLogHeader() {
|
||||
}
|
||||
|
||||
func (gui *Gui) getRandomTip() string {
|
||||
config := gui.Config.GetUserConfig().Keybinding
|
||||
config := gui.UserConfig.Keybinding
|
||||
|
||||
formattedKey := func(key string) string {
|
||||
return gui.getKeyDisplay(key)
|
||||
|
||||
@@ -31,9 +31,12 @@ func (gui *Gui) getSelectedCommitFilePath() string {
|
||||
return node.GetPath()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitFileSelect() error {
|
||||
func (gui *Gui) onCommitFileFocus() error {
|
||||
gui.escapeLineByLinePanel()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) commitFilesRenderToMain() error {
|
||||
node := gui.getSelectedCommitFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
@@ -42,10 +45,8 @@ func (gui *Gui) handleCommitFileSelect() error {
|
||||
to := gui.State.CommitFileManager.GetParent()
|
||||
from, reverse := gui.getFromAndReverseArgsForDiff(to)
|
||||
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.ShowFileDiffCmdStr(from, to, reverse, node.GetPath(), false),
|
||||
)
|
||||
task := NewRunPtyTask(cmd)
|
||||
cmdObj := gui.GitCommand.ShowFileDiffCmdObj(from, to, reverse, node.GetPath(), false)
|
||||
task := NewRunPtyTask(cmdObj.GetCmd())
|
||||
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
main: &viewUpdateOpts{
|
||||
@@ -104,7 +105,7 @@ func (gui *Gui) refreshCommitFilesView() error {
|
||||
to := gui.State.Panels.CommitFiles.refName
|
||||
from, reverse := gui.getFromAndReverseArgsForDiff(to)
|
||||
|
||||
files, err := gui.GitCommand.GetFilesInDiff(from, to, reverse)
|
||||
files, err := gui.GitCommand.Loaders.CommitFiles.GetFilesInDiff(from, to, reverse)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
@@ -198,10 +199,10 @@ func (gui *Gui) startPatchManager() error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleEnterCommitFile() error {
|
||||
return gui.enterCommitFile(-1)
|
||||
return gui.enterCommitFile(OnFocusOpts{ClickedViewName: "", ClickedViewLineIdx: -1})
|
||||
}
|
||||
|
||||
func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
|
||||
func (gui *Gui) enterCommitFile(opts OnFocusOpts) error {
|
||||
node := gui.getSelectedCommitFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
@@ -211,17 +212,14 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
|
||||
return gui.handleToggleCommitFileDirCollapsed()
|
||||
}
|
||||
|
||||
enterTheFile := func(selectedLineIdx int) error {
|
||||
enterTheFile := func() error {
|
||||
if !gui.GitCommand.PatchManager.Active() {
|
||||
if err := gui.startPatchManager(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := gui.pushContext(gui.State.Contexts.PatchBuilding); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.handleRefreshPatchBuildingPanel(selectedLineIdx)
|
||||
return gui.pushContext(gui.State.Contexts.PatchBuilding, opts)
|
||||
}
|
||||
|
||||
if gui.GitCommand.PatchManager.Active() && gui.GitCommand.PatchManager.To != gui.State.CommitFileManager.GetParent() {
|
||||
@@ -231,7 +229,7 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
|
||||
handlersManageFocus: true,
|
||||
handleConfirm: func() error {
|
||||
gui.GitCommand.PatchManager.Reset()
|
||||
return enterTheFile(selectedLineIdx)
|
||||
return enterTheFile()
|
||||
},
|
||||
handleClose: func() error {
|
||||
return gui.pushContext(gui.State.Contexts.CommitFiles)
|
||||
@@ -239,7 +237,7 @@ func (gui *Gui) enterCommitFile(selectedLineIdx int) error {
|
||||
})
|
||||
}
|
||||
|
||||
return enterTheFile(selectedLineIdx)
|
||||
return enterTheFile()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleToggleCommitFileDirCollapsed() error {
|
||||
|
||||
@@ -14,17 +14,22 @@ func (gui *Gui) handleCommitConfirm() error {
|
||||
if message == "" {
|
||||
return gui.createErrorPanel(gui.Tr.CommitWithoutMessageErr)
|
||||
}
|
||||
flags := ""
|
||||
skipHookPrefix := gui.Config.GetUserConfig().Git.SkipHookPrefix
|
||||
flags := []string{}
|
||||
skipHookPrefix := gui.UserConfig.Git.SkipHookPrefix
|
||||
if skipHookPrefix != "" && strings.HasPrefix(message, skipHookPrefix) {
|
||||
flags = "--no-verify"
|
||||
flags = append(flags, "--no-verify")
|
||||
}
|
||||
|
||||
cmdStr := gui.GitCommand.CommitCmdStr(message, flags)
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(cmdStr, gui.Tr.Spans.Commit, true))
|
||||
return gui.withGpgHandling(cmdStr, gui.Tr.CommittingStatus, func() error {
|
||||
if gui.UserConfig.Git.Commit.SignOff {
|
||||
flags = append(flags, "--signoff")
|
||||
}
|
||||
|
||||
cmdObj := gui.GitCommand.CommitCmdObj(message, strings.Join(flags, " "))
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(cmdObj.ToString(), gui.Tr.Spans.Commit, true))
|
||||
|
||||
_ = gui.returnFromContext()
|
||||
return gui.withGpgHandling(cmdObj, gui.Tr.CommittingStatus, func() error {
|
||||
gui.Views.CommitMessage.ClearTextArea()
|
||||
_ = gui.returnFromContext()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -37,9 +42,9 @@ func (gui *Gui) handleCommitMessageFocused() error {
|
||||
message := utils.ResolvePlaceholderString(
|
||||
gui.Tr.CommitMessageConfirm,
|
||||
map[string]string{
|
||||
"keyBindClose": gui.getKeyDisplay(gui.Config.GetUserConfig().Keybinding.Universal.Return),
|
||||
"keyBindConfirm": gui.getKeyDisplay(gui.Config.GetUserConfig().Keybinding.Universal.Confirm),
|
||||
"keyBindNewLine": gui.getKeyDisplay(gui.Config.GetUserConfig().Keybinding.Universal.AppendNewline),
|
||||
"keyBindClose": gui.getKeyDisplay(gui.UserConfig.Keybinding.Universal.Return),
|
||||
"keyBindConfirm": gui.getKeyDisplay(gui.UserConfig.Keybinding.Universal.Confirm),
|
||||
"keyBindNewLine": gui.getKeyDisplay(gui.UserConfig.Keybinding.Universal.AppendNewline),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -53,7 +58,7 @@ func (gui *Gui) getBufferLength(view *gocui.View) string {
|
||||
|
||||
// RenderCommitLength is a function.
|
||||
func (gui *Gui) RenderCommitLength() {
|
||||
if !gui.Config.GetUserConfig().Gui.CommitLength.Show {
|
||||
if !gui.UserConfig.Gui.CommitLength.Show {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+168
-22
@@ -4,12 +4,15 @@ import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"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/utils"
|
||||
)
|
||||
|
||||
// after selecting the 200th commit, we'll load in all the rest
|
||||
const COMMIT_THRESHOLD = 200
|
||||
|
||||
// list panel functions
|
||||
|
||||
func (gui *Gui) getSelectedLocalCommit() *models.Commit {
|
||||
@@ -21,9 +24,9 @@ func (gui *Gui) getSelectedLocalCommit() *models.Commit {
|
||||
return gui.State.Commits[selectedLine]
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitSelect() error {
|
||||
func (gui *Gui) onCommitFocus() error {
|
||||
state := gui.State.Panels.Commits
|
||||
if state.SelectedLineIdx > 290 && state.LimitCommits {
|
||||
if state.SelectedLineIdx > COMMIT_THRESHOLD && state.LimitCommits {
|
||||
state.LimitCommits = false
|
||||
go utils.Safe(func() {
|
||||
if err := gui.refreshCommitsWithLimit(); err != nil {
|
||||
@@ -34,15 +37,17 @@ func (gui *Gui) handleCommitSelect() error {
|
||||
|
||||
gui.escapeLineByLinePanel()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) branchCommitsRenderToMain() error {
|
||||
var task updateTask
|
||||
commit := gui.getSelectedLocalCommit()
|
||||
if commit == nil {
|
||||
task = NewRenderStringTask(gui.Tr.NoCommitsThisBranch)
|
||||
} else {
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.Modes.Filtering.GetPath()),
|
||||
)
|
||||
task = NewRunPtyTask(cmd)
|
||||
cmdObj := gui.GitCommand.ShowCmdObj(commit.Sha, gui.State.Modes.Filtering.GetPath())
|
||||
task = NewRunPtyTask(cmdObj.GetCmd())
|
||||
}
|
||||
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
@@ -114,14 +119,13 @@ func (gui *Gui) refreshCommitsWithLimit() error {
|
||||
gui.Mutexes.BranchCommitsMutex.Lock()
|
||||
defer gui.Mutexes.BranchCommitsMutex.Unlock()
|
||||
|
||||
builder := commands.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr)
|
||||
|
||||
commits, err := builder.GetCommits(
|
||||
commands.GetCommitsOptions{
|
||||
commits, err := gui.GitCommand.Loaders.Commits.GetCommits(
|
||||
loaders.GetCommitsOptions{
|
||||
Limit: gui.State.Panels.Commits.LimitCommits,
|
||||
FilterPath: gui.State.Modes.Filtering.GetPath(),
|
||||
IncludeRebaseCommits: true,
|
||||
RefName: "HEAD",
|
||||
All: gui.State.ShowWholeGitGraph,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@@ -136,9 +140,7 @@ func (gui *Gui) refreshRebaseCommits() error {
|
||||
gui.Mutexes.BranchCommitsMutex.Lock()
|
||||
defer gui.Mutexes.BranchCommitsMutex.Unlock()
|
||||
|
||||
builder := commands.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr)
|
||||
|
||||
updatedCommits, err := builder.MergeRebasingCommits(gui.State.Commits)
|
||||
updatedCommits, err := gui.GitCommand.Loaders.Commits.MergeRebasingCommits(gui.State.Commits)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -571,25 +573,63 @@ func (gui *Gui) handleSquashAllAboveFixupCommits() error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleTagCommit() error {
|
||||
// TODO: bring up menu asking if you want to make a lightweight or annotated tag
|
||||
// if annotated, switch to a subprocess to create the message
|
||||
|
||||
commit := gui.getSelectedLocalCommit()
|
||||
if commit == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.handleCreateLightweightTag(commit.Sha)
|
||||
return gui.createTagMenu(commit.Sha)
|
||||
}
|
||||
|
||||
func (gui *Gui) createTagMenu(commitSha string) error {
|
||||
items := []*menuItem{
|
||||
{
|
||||
displayString: gui.Tr.LcLightweightTag,
|
||||
onPress: func() error {
|
||||
return gui.handleCreateLightweightTag(commitSha)
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: gui.Tr.LcAnnotatedTag,
|
||||
onPress: func() error {
|
||||
return gui.handleCreateAnnotatedTag(commitSha)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return gui.createMenu(gui.Tr.TagMenuTitle, items, createMenuOptions{showCancel: true})
|
||||
}
|
||||
|
||||
func (gui *Gui) afterTagCreate(tagName string) error {
|
||||
gui.State.Panels.Tags.SelectedLineIdx = 0 // Set to the top
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{COMMITS, TAGS}})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateAnnotatedTag(commitSha string) error {
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.TagNameTitle,
|
||||
handleConfirm: func(tagName string) error {
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.TagMessageTitle,
|
||||
handleConfirm: func(msg string) error {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.CreateAnnotatedTag).CreateAnnotatedTag(tagName, commitSha, msg); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.afterTagCreate(tagName)
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreateLightweightTag(commitSha string) error {
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.TagNameTitle,
|
||||
handleConfirm: func(response string) error {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.CreateLightweightTag).CreateLightweightTag(response, commitSha); err != nil {
|
||||
handleConfirm: func(tagName string) error {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.CreateLightweightTag).CreateLightweightTag(tagName, commitSha); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{COMMITS, TAGS}})
|
||||
return gui.afterTagCreate(tagName)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -640,7 +680,7 @@ func (gui *Gui) handleGotoBottomForCommitsPanel() error {
|
||||
}
|
||||
|
||||
for _, context := range gui.getListContexts() {
|
||||
if context.ViewName == "commits" {
|
||||
if context.GetViewName() == "commits" {
|
||||
return context.handleGotoBottom()
|
||||
}
|
||||
}
|
||||
@@ -667,3 +707,109 @@ func (gui *Gui) handleCopySelectedCommitMessageToClipboard() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleOpenLogMenu() error {
|
||||
return gui.createMenu(gui.Tr.LogMenuTitle, []*menuItem{
|
||||
{
|
||||
displayString: gui.Tr.ToggleShowGitGraphAll,
|
||||
onPress: func() error {
|
||||
gui.State.ShowWholeGitGraph = !gui.State.ShowWholeGitGraph
|
||||
|
||||
if gui.State.ShowWholeGitGraph {
|
||||
gui.State.Panels.Commits.LimitCommits = false
|
||||
}
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.LcLoadingCommits, func() error {
|
||||
return gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []RefreshableView{COMMITS}})
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: gui.Tr.ShowGitGraph,
|
||||
opensMenu: true,
|
||||
onPress: func() error {
|
||||
onSelect := func(value string) {
|
||||
gui.UserConfig.Git.Log.ShowGraph = value
|
||||
gui.render()
|
||||
}
|
||||
return gui.createMenu(gui.Tr.LogMenuTitle, []*menuItem{
|
||||
{
|
||||
displayString: "always",
|
||||
onPress: func() error {
|
||||
onSelect("always")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: "never",
|
||||
onPress: func() error {
|
||||
onSelect("never")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: "when maximised",
|
||||
onPress: func() error {
|
||||
onSelect("when-maximised")
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}, createMenuOptions{showCancel: true})
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: gui.Tr.SortCommits,
|
||||
opensMenu: true,
|
||||
onPress: func() error {
|
||||
onSelect := func(value string) error {
|
||||
gui.UserConfig.Git.Log.Order = value
|
||||
return gui.WithWaitingStatus(gui.Tr.LcLoadingCommits, func() error {
|
||||
return gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []RefreshableView{COMMITS}})
|
||||
})
|
||||
}
|
||||
return gui.createMenu(gui.Tr.LogMenuTitle, []*menuItem{
|
||||
{
|
||||
displayString: "topological (topo-order)",
|
||||
onPress: func() error {
|
||||
return onSelect("topo-order")
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: "date-order",
|
||||
onPress: func() error {
|
||||
return onSelect("date-order")
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: "author-date-order",
|
||||
onPress: func() error {
|
||||
return onSelect("author-date-order")
|
||||
},
|
||||
},
|
||||
}, createMenuOptions{showCancel: true})
|
||||
},
|
||||
},
|
||||
}, createMenuOptions{showCancel: true})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleOpenCommitInBrowser() error {
|
||||
commit := gui.getSelectedLocalCommit()
|
||||
if commit == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
hostingServiceMgr := gui.getHostingServiceMgr()
|
||||
|
||||
url, err := hostingServiceMgr.GetCommitURL(commit.Sha)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
if err := gui.GitCommand.OSCommand.OpenLink(url); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(fmt.Sprintf(gui.Tr.OpeningCommitInBrowser, url), gui.Tr.CreatePullRequest, false))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// lots of this has been directly ported from one of the example files, will brush up later
|
||||
|
||||
package gui
|
||||
|
||||
import (
|
||||
@@ -7,7 +5,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
@@ -44,30 +41,15 @@ type promptOpts struct {
|
||||
}
|
||||
|
||||
func (gui *Gui) ask(opts askOpts) error {
|
||||
return gui.createPopupPanel(createPopupPanelOpts{
|
||||
title: opts.title,
|
||||
prompt: opts.prompt,
|
||||
handleConfirm: opts.handleConfirm,
|
||||
handleClose: opts.handleClose,
|
||||
handlersManageFocus: opts.handlersManageFocus,
|
||||
})
|
||||
return gui.PopupHandler.Ask(opts)
|
||||
}
|
||||
|
||||
func (gui *Gui) prompt(opts promptOpts) error {
|
||||
return gui.createPopupPanel(createPopupPanelOpts{
|
||||
title: opts.title,
|
||||
prompt: opts.initialContent,
|
||||
editable: true,
|
||||
handleConfirmPrompt: opts.handleConfirm,
|
||||
findSuggestionsFunc: opts.findSuggestionsFunc,
|
||||
})
|
||||
return gui.PopupHandler.Prompt(opts)
|
||||
}
|
||||
|
||||
func (gui *Gui) createLoaderPanel(prompt string) error {
|
||||
return gui.createPopupPanel(createPopupPanelOpts{
|
||||
prompt: prompt,
|
||||
hasLoader: true,
|
||||
})
|
||||
return gui.PopupHandler.Loader(prompt)
|
||||
}
|
||||
|
||||
func (gui *Gui) wrappedConfirmationFunction(handlersManageFocus bool, function func() error) func() error {
|
||||
@@ -191,7 +173,7 @@ func (gui *Gui) prepareConfirmationPanel(
|
||||
suggestionsView.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.setSuggestions(findSuggestionsFunc(""))
|
||||
suggestionsView.Visible = true
|
||||
suggestionsView.Title = fmt.Sprintf(gui.Tr.SuggestionsTitle, gui.Config.GetUserConfig().Keybinding.Universal.TogglePanel)
|
||||
suggestionsView.Title = fmt.Sprintf(gui.Tr.SuggestionsTitle, gui.UserConfig.Keybinding.Universal.TogglePanel)
|
||||
}
|
||||
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
@@ -258,7 +240,7 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
|
||||
handler func() error
|
||||
}
|
||||
|
||||
keybindingConfig := gui.Config.GetUserConfig().Keybinding
|
||||
keybindingConfig := gui.UserConfig.Keybinding
|
||||
onSuggestionConfirm := gui.wrappedPromptConfirmationFunction(
|
||||
opts.handlersManageFocus,
|
||||
opts.handleConfirmPrompt,
|
||||
@@ -323,7 +305,7 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) clearConfirmationViewKeyBindings() {
|
||||
keybindingConfig := gui.Config.GetUserConfig().Keybinding
|
||||
keybindingConfig := gui.UserConfig.Keybinding
|
||||
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.Confirm), gocui.ModNone)
|
||||
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone)
|
||||
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.Return), gocui.ModNone)
|
||||
@@ -339,15 +321,7 @@ func (gui *Gui) wrappedHandler(f func() error) func(g *gocui.Gui, v *gocui.View)
|
||||
}
|
||||
|
||||
func (gui *Gui) createErrorPanel(message string) error {
|
||||
coloredMessage := style.FgRed.Sprint(strings.TrimSpace(message))
|
||||
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.Error,
|
||||
prompt: coloredMessage,
|
||||
})
|
||||
return gui.PopupHandler.Error(message)
|
||||
}
|
||||
|
||||
func (gui *Gui) surfaceError(err error) error {
|
||||
|
||||
+24
-10
@@ -1,6 +1,7 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
@@ -16,10 +17,16 @@ const (
|
||||
EXTRAS_CONTEXT
|
||||
)
|
||||
|
||||
type OnFocusOpts struct {
|
||||
ClickedViewName string
|
||||
ClickedViewLineIdx int
|
||||
}
|
||||
|
||||
type Context interface {
|
||||
HandleFocus() error
|
||||
HandleFocus(opts ...OnFocusOpts) error
|
||||
HandleFocusLost() error
|
||||
HandleRender() error
|
||||
HandleRenderToMain() error
|
||||
GetKind() ContextKind
|
||||
GetViewName() string
|
||||
GetWindowName() string
|
||||
@@ -81,15 +88,20 @@ func (gui *Gui) replaceContext(c Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) pushContext(c Context) error {
|
||||
func (gui *Gui) pushContext(c Context, opts ...OnFocusOpts) error {
|
||||
// using triple dot but you should only ever pass one of these opt structs
|
||||
if len(opts) > 1 {
|
||||
return errors.New("cannot pass multiple opts to pushContext")
|
||||
}
|
||||
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
return gui.pushContextDirect(c)
|
||||
return gui.pushContextDirect(c, opts...)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) pushContextDirect(c Context) error {
|
||||
func (gui *Gui) pushContextDirect(c Context, opts ...OnFocusOpts) error {
|
||||
gui.State.ContextManager.Lock()
|
||||
|
||||
// push onto stack
|
||||
@@ -114,7 +126,7 @@ func (gui *Gui) pushContextDirect(c Context) error {
|
||||
|
||||
gui.State.ContextManager.Unlock()
|
||||
|
||||
return gui.activateContext(c)
|
||||
return gui.activateContext(c, opts...)
|
||||
}
|
||||
|
||||
// asynchronous code idea: functions return an error via a channel, when done
|
||||
@@ -169,7 +181,7 @@ func (gui *Gui) deactivateContext(c Context) error {
|
||||
}
|
||||
|
||||
// if we are the kind of context that is sent to back upon deactivation, we should do that
|
||||
if view != nil && c.GetKind() == TEMPORARY_POPUP || c.GetKind() == PERSISTENT_POPUP || c.GetKey() == COMMIT_FILES_CONTEXT_KEY {
|
||||
if view != nil && (c.GetKind() == TEMPORARY_POPUP || c.GetKind() == PERSISTENT_POPUP || c.GetKey() == COMMIT_FILES_CONTEXT_KEY) {
|
||||
view.Visible = false
|
||||
}
|
||||
|
||||
@@ -206,7 +218,7 @@ func (gui *Gui) postRefreshUpdate(c Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) activateContext(c Context) error {
|
||||
func (gui *Gui) activateContext(c Context, opts ...OnFocusOpts) error {
|
||||
viewName := c.GetViewName()
|
||||
v, err := gui.g.View(viewName)
|
||||
if err != nil {
|
||||
@@ -249,7 +261,7 @@ func (gui *Gui) activateContext(c Context) error {
|
||||
}
|
||||
gui.renderOptionsMap(optionsMap)
|
||||
|
||||
if err := c.HandleFocus(); err != nil {
|
||||
if err := c.HandleFocus(opts...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -285,9 +297,9 @@ func (gui *Gui) currentContextWithoutLock() Context {
|
||||
|
||||
// the status panel is not yet a list context (and may never be), so this method is not
|
||||
// quite the same as currentSideContext()
|
||||
func (gui *Gui) currentSideListContext() *ListContext {
|
||||
func (gui *Gui) currentSideListContext() IListContext {
|
||||
context := gui.currentSideContext()
|
||||
listContext, ok := context.(*ListContext)
|
||||
listContext, ok := context.(IListContext)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
@@ -399,6 +411,8 @@ func (gui *Gui) onViewFocusLost(oldView *gocui.View, newView *gocui.View) error
|
||||
return nil
|
||||
}
|
||||
|
||||
_ = oldView.SetOriginX(0)
|
||||
|
||||
if oldView == gui.Views.CommitFiles && newView != gui.Views.Main && newView != gui.Views.Secondary && newView != gui.Views.Search {
|
||||
gui.resetWindowForView(gui.Views.CommitFiles)
|
||||
if err := gui.deactivateContext(gui.State.Contexts.CommitFiles); err != nil {
|
||||
|
||||
+48
-32
@@ -56,19 +56,19 @@ var allContextKeys = []ContextKey{
|
||||
|
||||
type ContextTree struct {
|
||||
Status Context
|
||||
Files *ListContext
|
||||
Submodules *ListContext
|
||||
Menu *ListContext
|
||||
Branches *ListContext
|
||||
Remotes *ListContext
|
||||
RemoteBranches *ListContext
|
||||
Tags *ListContext
|
||||
BranchCommits *ListContext
|
||||
CommitFiles *ListContext
|
||||
ReflogCommits *ListContext
|
||||
SubCommits *ListContext
|
||||
Stash *ListContext
|
||||
Suggestions *ListContext
|
||||
Files IListContext
|
||||
Submodules IListContext
|
||||
Menu IListContext
|
||||
Branches IListContext
|
||||
Remotes IListContext
|
||||
RemoteBranches IListContext
|
||||
Tags IListContext
|
||||
BranchCommits IListContext
|
||||
CommitFiles IListContext
|
||||
ReflogCommits IListContext
|
||||
SubCommits IListContext
|
||||
Stash IListContext
|
||||
Suggestions IListContext
|
||||
Normal Context
|
||||
Staging Context
|
||||
PatchBuilding Context
|
||||
@@ -110,10 +110,10 @@ func (gui *Gui) allContexts() []Context {
|
||||
func (gui *Gui) contextTree() ContextTree {
|
||||
return ContextTree{
|
||||
Status: &BasicContext{
|
||||
OnFocus: gui.handleStatusSelect,
|
||||
Kind: SIDE_CONTEXT,
|
||||
ViewName: "status",
|
||||
Key: STATUS_CONTEXT_KEY,
|
||||
OnRenderToMain: OnFocusWrapper(gui.statusRenderToMain),
|
||||
Kind: SIDE_CONTEXT,
|
||||
ViewName: "status",
|
||||
Key: STATUS_CONTEXT_KEY,
|
||||
},
|
||||
Files: gui.filesListContext(),
|
||||
Submodules: gui.submodulesListContext(),
|
||||
@@ -128,7 +128,7 @@ func (gui *Gui) contextTree() ContextTree {
|
||||
Tags: gui.tagsListContext(),
|
||||
Stash: gui.stashListContext(),
|
||||
Normal: &BasicContext{
|
||||
OnFocus: func() error {
|
||||
OnFocus: func(opts ...OnFocusOpts) error {
|
||||
return nil // TODO: should we do something here? We should allow for scrolling the panel
|
||||
},
|
||||
Kind: MAIN_CONTEXT,
|
||||
@@ -136,59 +136,67 @@ func (gui *Gui) contextTree() ContextTree {
|
||||
Key: MAIN_NORMAL_CONTEXT_KEY,
|
||||
},
|
||||
Staging: &BasicContext{
|
||||
OnFocus: func() error {
|
||||
return nil
|
||||
// TODO: centralise the code here
|
||||
// return gui.refreshStagingPanel(false, -1)
|
||||
OnRenderToMain: func(opts ...OnFocusOpts) error {
|
||||
forceSecondaryFocused := false
|
||||
selectedLineIdx := -1
|
||||
if len(opts) > 0 && opts[0].ClickedViewName != "" {
|
||||
if opts[0].ClickedViewName == "main" || opts[0].ClickedViewName == "secondary" {
|
||||
selectedLineIdx = opts[0].ClickedViewLineIdx
|
||||
}
|
||||
if opts[0].ClickedViewName == "secondary" {
|
||||
forceSecondaryFocused = true
|
||||
}
|
||||
}
|
||||
return gui.handleRefreshStagingPanel(forceSecondaryFocused, selectedLineIdx)
|
||||
},
|
||||
Kind: MAIN_CONTEXT,
|
||||
ViewName: "main",
|
||||
Key: MAIN_STAGING_CONTEXT_KEY,
|
||||
},
|
||||
PatchBuilding: &BasicContext{
|
||||
OnFocus: func() error {
|
||||
return nil
|
||||
// TODO: centralise the code here
|
||||
// return gui.refreshPatchBuildingPanel(-1)
|
||||
OnRenderToMain: func(opts ...OnFocusOpts) error {
|
||||
selectedLineIdx := -1
|
||||
if len(opts) > 0 && (opts[0].ClickedViewName == "main" || opts[0].ClickedViewName == "secondary") {
|
||||
selectedLineIdx = opts[0].ClickedViewLineIdx
|
||||
}
|
||||
|
||||
return gui.handleRefreshPatchBuildingPanel(selectedLineIdx)
|
||||
},
|
||||
Kind: MAIN_CONTEXT,
|
||||
ViewName: "main",
|
||||
Key: MAIN_PATCH_BUILDING_CONTEXT_KEY,
|
||||
},
|
||||
Merging: &BasicContext{
|
||||
OnFocus: gui.refreshMergePanelWithLock,
|
||||
OnFocus: OnFocusWrapper(gui.refreshMergePanelWithLock),
|
||||
Kind: MAIN_CONTEXT,
|
||||
ViewName: "main",
|
||||
Key: MAIN_MERGING_CONTEXT_KEY,
|
||||
OnGetOptionsMap: gui.getMergingOptions,
|
||||
},
|
||||
Credentials: &BasicContext{
|
||||
OnFocus: gui.handleCredentialsViewFocused,
|
||||
OnFocus: OnFocusWrapper(gui.handleCredentialsViewFocused),
|
||||
Kind: PERSISTENT_POPUP,
|
||||
ViewName: "credentials",
|
||||
Key: CREDENTIALS_CONTEXT_KEY,
|
||||
},
|
||||
Confirmation: &BasicContext{
|
||||
OnFocus: func() error { return nil },
|
||||
Kind: TEMPORARY_POPUP,
|
||||
ViewName: "confirmation",
|
||||
Key: CONFIRMATION_CONTEXT_KEY,
|
||||
},
|
||||
Suggestions: gui.suggestionsListContext(),
|
||||
CommitMessage: &BasicContext{
|
||||
OnFocus: gui.handleCommitMessageFocused,
|
||||
OnFocus: OnFocusWrapper(gui.handleCommitMessageFocused),
|
||||
Kind: PERSISTENT_POPUP,
|
||||
ViewName: "commitMessage",
|
||||
Key: COMMIT_MESSAGE_CONTEXT_KEY,
|
||||
},
|
||||
Search: &BasicContext{
|
||||
OnFocus: func() error { return nil },
|
||||
Kind: PERSISTENT_POPUP,
|
||||
ViewName: "search",
|
||||
Key: SEARCH_CONTEXT_KEY,
|
||||
},
|
||||
CommandLog: &BasicContext{
|
||||
OnFocus: func() error { return nil },
|
||||
Kind: EXTRAS_CONTEXT,
|
||||
ViewName: "extras",
|
||||
Key: COMMAND_LOG_CONTEXT_KEY,
|
||||
@@ -201,6 +209,14 @@ func (gui *Gui) contextTree() ContextTree {
|
||||
}
|
||||
}
|
||||
|
||||
// using this wrapper for when an onFocus function doesn't care about any potential
|
||||
// props that could be passed
|
||||
func OnFocusWrapper(f func() error) func(opts ...OnFocusOpts) error {
|
||||
return func(opts ...OnFocusOpts) error {
|
||||
return f()
|
||||
}
|
||||
}
|
||||
|
||||
func (tree ContextTree) initialViewContextMap() map[string]Context {
|
||||
return map[string]Context{
|
||||
"status": tree.Status,
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCanDeactivatePopupContextsWithoutViews(t *testing.T) {
|
||||
contexts := []func(gui *Gui) Context{
|
||||
func(gui *Gui) Context { return gui.State.Contexts.Credentials },
|
||||
func(gui *Gui) Context { return gui.State.Contexts.Confirmation },
|
||||
func(gui *Gui) Context { return gui.State.Contexts.CommitMessage },
|
||||
func(gui *Gui) Context { return gui.State.Contexts.Search },
|
||||
}
|
||||
|
||||
for _, c := range contexts {
|
||||
gui := NewDummyGui()
|
||||
context := c(gui)
|
||||
gui.g = &gocui.Gui{}
|
||||
|
||||
_ = gui.deactivateContext(context)
|
||||
|
||||
// This really only checks a prerequisit, not the effect of deactivateContext
|
||||
view, _ := gui.g.View(context.GetViewName())
|
||||
assert.Nil(t, view, string(context.GetKey()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanDeactivateCommitFilesContextsWithoutViews(t *testing.T) {
|
||||
gui := NewDummyGui()
|
||||
gui.g = &gocui.Gui{}
|
||||
|
||||
_ = gui.deactivateContext(gui.State.Contexts.CommitFiles)
|
||||
|
||||
// This really only checks a prerequisite, not the effect of deactivateContext
|
||||
view, _ := gui.g.View(gui.State.Contexts.CommitFiles.GetViewName())
|
||||
assert.Nil(t, view)
|
||||
}
|
||||
@@ -57,7 +57,7 @@ func (gui *Gui) handleCloseCredentialsView() error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCredentialsViewFocused() error {
|
||||
keybindingConfig := gui.Config.GetUserConfig().Keybinding
|
||||
keybindingConfig := gui.UserConfig.Keybinding
|
||||
|
||||
message := utils.ResolvePlaceholderString(
|
||||
gui.Tr.CloseConfirm,
|
||||
|
||||
@@ -203,7 +203,7 @@ func (gui *Gui) menuPromptFromCommand(prompt config.CustomCommandPrompt, promptR
|
||||
}
|
||||
|
||||
// Run and save output
|
||||
message, err := gui.GitCommand.RunCommandWithOutput(cmdStr)
|
||||
message, err := gui.GitCommand.Cmd.New(cmdStr).RunWithOutput()
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
@@ -244,7 +244,7 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
|
||||
}
|
||||
|
||||
if customCommand.Subprocess {
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(gui.OSCommand.PrepareShellSubProcess(cmdStr))
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(gui.OSCommand.Cmd.NewShell(cmdStr))
|
||||
}
|
||||
|
||||
loadingText := customCommand.LoadingText
|
||||
@@ -252,7 +252,8 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
|
||||
loadingText = gui.Tr.LcRunningCustomCommandStatus
|
||||
}
|
||||
return gui.WithWaitingStatus(loadingText, func() error {
|
||||
if err := gui.OSCommand.WithSpan(gui.Tr.Spans.CustomCommand).RunShellCommand(cmdStr); err != nil {
|
||||
err := gui.OSCommand.WithSpan(gui.Tr.Spans.CustomCommand).Cmd.NewShell(cmdStr).Run()
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{})
|
||||
@@ -295,7 +296,7 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
|
||||
|
||||
func (gui *Gui) GetCustomCommandKeybindings() []*Binding {
|
||||
bindings := []*Binding{}
|
||||
customCommands := gui.Config.GetUserConfig().CustomCommands
|
||||
customCommands := gui.UserConfig.CustomCommands
|
||||
|
||||
for _, customCommand := range customCommands {
|
||||
var viewName string
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
func isShowingDiff(gui *Gui) bool {
|
||||
key := gui.currentStaticContext().GetKey()
|
||||
|
||||
return key == FILES_CONTEXT_KEY || key == COMMIT_FILES_CONTEXT_KEY || key == STASH_CONTEXT_KEY || key == BRANCH_COMMITS_CONTEXT_KEY || key == SUB_COMMITS_CONTEXT_KEY || key == MAIN_STAGING_CONTEXT_KEY || key == MAIN_PATCH_BUILDING_CONTEXT_KEY
|
||||
}
|
||||
|
||||
func (gui *Gui) IncreaseContextInDiffView() error {
|
||||
if isShowingDiff(gui) {
|
||||
if err := gui.CheckCanChangeContext(); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
gui.UserConfig.Git.DiffContextSize = gui.UserConfig.Git.DiffContextSize + 1
|
||||
return gui.currentStaticContext().HandleRenderToMain()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) DecreaseContextInDiffView() error {
|
||||
old_size := gui.UserConfig.Git.DiffContextSize
|
||||
|
||||
if isShowingDiff(gui) && old_size > 1 {
|
||||
if err := gui.CheckCanChangeContext(); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
gui.UserConfig.Git.DiffContextSize = old_size - 1
|
||||
return gui.currentStaticContext().HandleRenderToMain()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) CheckCanChangeContext() error {
|
||||
if gui.GitCommand.PatchManager.Active() {
|
||||
return errors.New(gui.Tr.CantChangeContextSizeError)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const diffForTest = `diff --git a/pkg/gui/diff_context_size.go b/pkg/gui/diff_context_size.go
|
||||
index 0da0a982..742b7dcf 100644
|
||||
--- a/pkg/gui/diff_context_size.go
|
||||
+++ b/pkg/gui/diff_context_size.go
|
||||
@@ -9,12 +9,12 @@ func getRefreshFunction(gui *Gui) func()error {
|
||||
}
|
||||
} else if key == MAIN_STAGING_CONTEXT_KEY {
|
||||
return func() error {
|
||||
- selectedLine := gui.Views.Secondary.SelectedLineIdx()
|
||||
+ selectedLine := gui.State.Panels.LineByLine.GetSelectedLineIdx()
|
||||
return gui.handleRefreshStagingPanel(false, selectedLine)
|
||||
}
|
||||
} else if key == MAIN_PATCH_BUILDING_CONTEXT_KEY {
|
||||
`
|
||||
|
||||
func setupGuiForTest(gui *Gui) {
|
||||
gui.g = &gocui.Gui{}
|
||||
gui.Views.Main, _ = gui.prepareView("main")
|
||||
gui.Views.Secondary, _ = gui.prepareView("secondary")
|
||||
gui.GitCommand.PatchManager = &patch.PatchManager{}
|
||||
_, _ = gui.refreshLineByLinePanel(diffForTest, "", false, 11)
|
||||
}
|
||||
|
||||
func TestIncreasesContextInDiffViewByOneInContextWithDiff(t *testing.T) {
|
||||
contexts := []func(gui *Gui) Context{
|
||||
func(gui *Gui) Context { return gui.State.Contexts.Files },
|
||||
func(gui *Gui) Context { return gui.State.Contexts.BranchCommits },
|
||||
func(gui *Gui) Context { return gui.State.Contexts.CommitFiles },
|
||||
func(gui *Gui) Context { return gui.State.Contexts.Stash },
|
||||
func(gui *Gui) Context { return gui.State.Contexts.Staging },
|
||||
func(gui *Gui) Context { return gui.State.Contexts.PatchBuilding },
|
||||
func(gui *Gui) Context { return gui.State.Contexts.SubCommits },
|
||||
}
|
||||
|
||||
for _, c := range contexts {
|
||||
gui := NewDummyGui()
|
||||
context := c(gui)
|
||||
setupGuiForTest(gui)
|
||||
gui.UserConfig.Git.DiffContextSize = 1
|
||||
_ = gui.pushContextDirect(context)
|
||||
|
||||
_ = gui.IncreaseContextInDiffView()
|
||||
|
||||
assert.Equal(t, 2, gui.UserConfig.Git.DiffContextSize, string(context.GetKey()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoesntIncreaseContextInDiffViewInContextWithoutDiff(t *testing.T) {
|
||||
contexts := []func(gui *Gui) Context{
|
||||
func(gui *Gui) Context { return gui.State.Contexts.Status },
|
||||
func(gui *Gui) Context { return gui.State.Contexts.Submodules },
|
||||
func(gui *Gui) Context { return gui.State.Contexts.Remotes },
|
||||
func(gui *Gui) Context { return gui.State.Contexts.Normal },
|
||||
func(gui *Gui) Context { return gui.State.Contexts.ReflogCommits },
|
||||
func(gui *Gui) Context { return gui.State.Contexts.RemoteBranches },
|
||||
func(gui *Gui) Context { return gui.State.Contexts.Tags },
|
||||
func(gui *Gui) Context { return gui.State.Contexts.Merging },
|
||||
func(gui *Gui) Context { return gui.State.Contexts.CommandLog },
|
||||
}
|
||||
|
||||
for _, c := range contexts {
|
||||
gui := NewDummyGui()
|
||||
context := c(gui)
|
||||
setupGuiForTest(gui)
|
||||
gui.UserConfig.Git.DiffContextSize = 1
|
||||
_ = gui.pushContextDirect(context)
|
||||
|
||||
_ = gui.IncreaseContextInDiffView()
|
||||
|
||||
assert.Equal(t, 1, gui.UserConfig.Git.DiffContextSize, string(context.GetKey()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecreasesContextInDiffViewByOneInContextWithDiff(t *testing.T) {
|
||||
contexts := []func(gui *Gui) Context{
|
||||
func(gui *Gui) Context { return gui.State.Contexts.Files },
|
||||
func(gui *Gui) Context { return gui.State.Contexts.BranchCommits },
|
||||
func(gui *Gui) Context { return gui.State.Contexts.CommitFiles },
|
||||
func(gui *Gui) Context { return gui.State.Contexts.Stash },
|
||||
func(gui *Gui) Context { return gui.State.Contexts.Staging },
|
||||
func(gui *Gui) Context { return gui.State.Contexts.PatchBuilding },
|
||||
func(gui *Gui) Context { return gui.State.Contexts.SubCommits },
|
||||
}
|
||||
|
||||
for _, c := range contexts {
|
||||
gui := NewDummyGui()
|
||||
context := c(gui)
|
||||
setupGuiForTest(gui)
|
||||
gui.UserConfig.Git.DiffContextSize = 2
|
||||
_ = gui.pushContextDirect(context)
|
||||
|
||||
_ = gui.DecreaseContextInDiffView()
|
||||
|
||||
assert.Equal(t, 1, gui.UserConfig.Git.DiffContextSize, string(context.GetKey()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoesntDecreaseContextInDiffViewInContextWithoutDiff(t *testing.T) {
|
||||
contexts := []func(gui *Gui) Context{
|
||||
func(gui *Gui) Context { return gui.State.Contexts.Status },
|
||||
func(gui *Gui) Context { return gui.State.Contexts.Submodules },
|
||||
func(gui *Gui) Context { return gui.State.Contexts.Remotes },
|
||||
func(gui *Gui) Context { return gui.State.Contexts.Normal },
|
||||
func(gui *Gui) Context { return gui.State.Contexts.ReflogCommits },
|
||||
func(gui *Gui) Context { return gui.State.Contexts.RemoteBranches },
|
||||
func(gui *Gui) Context { return gui.State.Contexts.Tags },
|
||||
func(gui *Gui) Context { return gui.State.Contexts.Merging },
|
||||
func(gui *Gui) Context { return gui.State.Contexts.CommandLog },
|
||||
}
|
||||
|
||||
for _, c := range contexts {
|
||||
gui := NewDummyGui()
|
||||
context := c(gui)
|
||||
setupGuiForTest(gui)
|
||||
gui.UserConfig.Git.DiffContextSize = 2
|
||||
_ = gui.pushContextDirect(context)
|
||||
|
||||
_ = gui.DecreaseContextInDiffView()
|
||||
|
||||
assert.Equal(t, 2, gui.UserConfig.Git.DiffContextSize, string(context.GetKey()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoesntIncreaseContextInDiffViewInContextWhenInPatchBuildingMode(t *testing.T) {
|
||||
gui := NewDummyGui()
|
||||
setupGuiForTest(gui)
|
||||
gui.UserConfig.Git.DiffContextSize = 2
|
||||
_ = gui.pushContextDirect(gui.State.Contexts.CommitFiles)
|
||||
gui.GitCommand.PatchManager.Start("from", "to", false, false)
|
||||
|
||||
errorCount := 0
|
||||
gui.PopupHandler = &TestPopupHandler{
|
||||
onError: func(message string) error {
|
||||
assert.Equal(t, gui.Tr.CantChangeContextSizeError, message)
|
||||
errorCount += 1
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
_ = gui.IncreaseContextInDiffView()
|
||||
|
||||
assert.Equal(t, 1, errorCount)
|
||||
assert.Equal(t, 2, gui.UserConfig.Git.DiffContextSize)
|
||||
}
|
||||
|
||||
func TestDoesntDecreaseContextInDiffViewInContextWhenInPatchBuildingMode(t *testing.T) {
|
||||
gui := NewDummyGui()
|
||||
setupGuiForTest(gui)
|
||||
gui.UserConfig.Git.DiffContextSize = 2
|
||||
_ = gui.pushContextDirect(gui.State.Contexts.CommitFiles)
|
||||
gui.GitCommand.PatchManager.Start("from", "to", false, false)
|
||||
|
||||
errorCount := 0
|
||||
gui.PopupHandler = &TestPopupHandler{
|
||||
onError: func(message string) error {
|
||||
assert.Equal(t, gui.Tr.CantChangeContextSizeError, message)
|
||||
errorCount += 1
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
_ = gui.DecreaseContextInDiffView()
|
||||
|
||||
assert.Equal(t, 2, gui.UserConfig.Git.DiffContextSize)
|
||||
}
|
||||
|
||||
func TestDecreasesContextInDiffViewNoFurtherThanOne(t *testing.T) {
|
||||
gui := NewDummyGui()
|
||||
setupGuiForTest(gui)
|
||||
gui.UserConfig.Git.DiffContextSize = 1
|
||||
|
||||
_ = gui.DecreaseContextInDiffView()
|
||||
|
||||
assert.Equal(t, 1, gui.UserConfig.Git.DiffContextSize)
|
||||
}
|
||||
+2
-2
@@ -13,10 +13,10 @@ func (gui *Gui) exitDiffMode() error {
|
||||
}
|
||||
|
||||
func (gui *Gui) renderDiff() error {
|
||||
cmd := gui.OSCommand.ExecutableFromString(
|
||||
cmdObj := gui.OSCommand.Cmd.New(
|
||||
fmt.Sprintf("git diff --submodule --no-ext-diff --color %s", gui.diffStr()),
|
||||
)
|
||||
task := NewRunPtyTask(cmd)
|
||||
task := NewRunPtyTask(cmdObj.GetCmd())
|
||||
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
main: &viewUpdateOpts{
|
||||
|
||||
+4
-5
@@ -4,7 +4,6 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/updates"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
@@ -12,12 +11,12 @@ import (
|
||||
// NewDummyGui creates a new dummy GUI for testing
|
||||
func NewDummyUpdater() *updates.Updater {
|
||||
newAppConfig := config.NewDummyAppConfig()
|
||||
DummyUpdater, _ := updates.NewUpdater(utils.NewDummyLog(), newAppConfig, oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog(), newAppConfig.GetUserConfig().Gui.Language))
|
||||
return DummyUpdater
|
||||
dummyUpdater, _ := updates.NewUpdater(utils.NewDummyCommon(), newAppConfig, oscommands.NewDummyOSCommand())
|
||||
return dummyUpdater
|
||||
}
|
||||
|
||||
func NewDummyGui() *Gui {
|
||||
newAppConfig := config.NewDummyAppConfig()
|
||||
DummyGui, _ := NewGui(utils.NewDummyLog(), commands.NewDummyGitCommand(), oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog(), newAppConfig.GetUserConfig().Gui.Language), newAppConfig, NewDummyUpdater(), "", false)
|
||||
return DummyGui
|
||||
dummyGui, _ := NewGui(utils.NewDummyCommon(), commands.NewDummyGitCommand(), oscommands.NewDummyOSCommand(), newAppConfig, NewDummyUpdater(), "", false)
|
||||
return dummyGui
|
||||
}
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func (gui *Gui) handleEditorKeypress(textArea *gocui.TextArea, key gocui.Key, ch rune, mod gocui.Modifier, allowMultiline bool) bool {
|
||||
newlineKey, ok := gui.getKey(gui.Config.GetUserConfig().Keybinding.Universal.AppendNewline).(gocui.Key)
|
||||
newlineKey, ok := gui.getKey(gui.UserConfig.Keybinding.Universal.AppendNewline).(gocui.Key)
|
||||
if !ok {
|
||||
newlineKey = gocui.KeyAltEnter
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
)
|
||||
|
||||
func (gui *Gui) handleCreateExtrasMenuPanel() error {
|
||||
menuItems := []*menuItem{
|
||||
{
|
||||
@@ -48,3 +54,28 @@ func (gui *Gui) scrollDownExtra() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) getCmdWriter() io.Writer {
|
||||
return &prefixWriter{writer: gui.Views.Extras, prefix: style.FgMagenta.Sprintf("\n\n%s\n", gui.Tr.GitOutput)}
|
||||
}
|
||||
|
||||
// Ensures that the first write is preceded by writing a prefix.
|
||||
// This allows us to say 'Git output:' before writing the actual git output.
|
||||
// We could just write directly to the view in this package before running the command but we already have code in the commands package that writes to the same view beforehand (with the command it's about to run) so things would be out of order.
|
||||
type prefixWriter struct {
|
||||
prefix string
|
||||
prefixWritten bool
|
||||
writer io.Writer
|
||||
}
|
||||
|
||||
func (self *prefixWriter) Write(p []byte) (n int, err error) {
|
||||
if !self.prefixWritten {
|
||||
self.prefixWritten = true
|
||||
// assuming we can write this prefix in one go
|
||||
_, err = self.writer.Write([]byte(self.prefix))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return self.writer.Write(p)
|
||||
}
|
||||
|
||||
+70
-48
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"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/config"
|
||||
@@ -42,9 +43,7 @@ func (gui *Gui) getSelectedPath() string {
|
||||
return node.GetPath()
|
||||
}
|
||||
|
||||
func (gui *Gui) selectFile(alreadySelected bool) error {
|
||||
gui.Views.Files.FocusPoint(0, gui.State.Panels.Files.SelectedLineIdx)
|
||||
|
||||
func (gui *Gui) filesRenderToMain() error {
|
||||
node := gui.getSelectedFileNode()
|
||||
|
||||
if node == nil {
|
||||
@@ -56,30 +55,24 @@ func (gui *Gui) selectFile(alreadySelected bool) error {
|
||||
})
|
||||
}
|
||||
|
||||
if !alreadySelected {
|
||||
gui.takeOverMergeConflictScrolling()
|
||||
}
|
||||
|
||||
if node.File != nil && node.File.HasInlineMergeConflicts {
|
||||
return gui.refreshMergePanelWithLock()
|
||||
}
|
||||
|
||||
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(node, false, !node.GetHasUnstagedChanges() && node.GetHasStagedChanges(), gui.State.IgnoreWhitespaceInDiffView)
|
||||
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
|
||||
cmdObj := gui.GitCommand.WorktreeFileDiffCmdObj(node, false, !node.GetHasUnstagedChanges() && node.GetHasStagedChanges(), gui.State.IgnoreWhitespaceInDiffView)
|
||||
|
||||
refreshOpts := refreshMainOpts{main: &viewUpdateOpts{
|
||||
title: gui.Tr.UnstagedChanges,
|
||||
task: NewRunPtyTask(cmd),
|
||||
task: NewRunPtyTask(cmdObj.GetCmd()),
|
||||
}}
|
||||
|
||||
if node.GetHasUnstagedChanges() {
|
||||
if node.GetHasStagedChanges() {
|
||||
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(node, false, true, gui.State.IgnoreWhitespaceInDiffView)
|
||||
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
|
||||
cmdObj := gui.GitCommand.WorktreeFileDiffCmdObj(node, false, true, gui.State.IgnoreWhitespaceInDiffView)
|
||||
|
||||
refreshOpts.secondary = &viewUpdateOpts{
|
||||
title: gui.Tr.StagedChanges,
|
||||
task: NewRunPtyTask(cmd),
|
||||
task: NewRunPtyTask(cmdObj.GetCmd()),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -121,9 +114,12 @@ func (gui *Gui) refreshFilesAndSubmodules() error {
|
||||
if gui.currentContext().GetKey() == FILES_CONTEXT_KEY || (g.CurrentView() == gui.Views.Main && ContextKey(g.CurrentView().Context) == MAIN_MERGING_CONTEXT_KEY) {
|
||||
newSelectedPath := gui.getSelectedPath()
|
||||
alreadySelected := selectedPath != "" && newSelectedPath == selectedPath
|
||||
if err := gui.selectFile(alreadySelected); err != nil {
|
||||
return err
|
||||
if !alreadySelected {
|
||||
gui.takeOverMergeConflictScrolling()
|
||||
}
|
||||
|
||||
gui.Views.Files.FocusPoint(0, gui.State.Panels.Files.SelectedLineIdx)
|
||||
return gui.filesRenderToMain()
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -166,10 +162,10 @@ func (gui *Gui) stageSelectedFile() error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleEnterFile() error {
|
||||
return gui.enterFile(false, -1)
|
||||
return gui.enterFile(OnFocusOpts{ClickedViewName: "", ClickedViewLineIdx: -1})
|
||||
}
|
||||
|
||||
func (gui *Gui) enterFile(forceSecondaryFocused bool, selectedLineIdx int) error {
|
||||
func (gui *Gui) enterFile(opts OnFocusOpts) error {
|
||||
node := gui.getSelectedFileNode()
|
||||
if node == nil {
|
||||
return nil
|
||||
@@ -193,9 +189,8 @@ func (gui *Gui) enterFile(forceSecondaryFocused bool, selectedLineIdx int) error
|
||||
if file.HasMergeConflicts {
|
||||
return gui.createErrorPanel(gui.Tr.FileStagingRequirements)
|
||||
}
|
||||
_ = gui.pushContext(gui.State.Contexts.Staging)
|
||||
|
||||
return gui.handleRefreshStagingPanel(forceSecondaryFocused, selectedLineIdx) // TODO: check if this is broken, try moving into context code
|
||||
return gui.pushContext(gui.State.Contexts.Staging, opts)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleFilePress() error {
|
||||
@@ -243,7 +238,7 @@ func (gui *Gui) handleFilePress() error {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.selectFile(true)
|
||||
return gui.State.Contexts.Files.HandleFocus()
|
||||
}
|
||||
|
||||
func (gui *Gui) allFilesStaged() bool {
|
||||
@@ -255,8 +250,9 @@ func (gui *Gui) allFilesStaged() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (gui *Gui) focusAndSelectFile() error {
|
||||
return gui.selectFile(false)
|
||||
func (gui *Gui) onFocusFile() error {
|
||||
gui.takeOverMergeConflictScrolling()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleStageAll() error {
|
||||
@@ -274,7 +270,7 @@ func (gui *Gui) handleStageAll() error {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.selectFile(false)
|
||||
return gui.State.Contexts.Files.HandleFocus()
|
||||
}
|
||||
|
||||
func (gui *Gui) handleIgnoreFile() error {
|
||||
@@ -335,7 +331,7 @@ func (gui *Gui) handleIgnoreFile() error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleWIPCommitPress() error {
|
||||
skipHookPrefix := gui.Config.GetUserConfig().Git.SkipHookPrefix
|
||||
skipHookPrefix := gui.UserConfig.Git.SkipHookPrefix
|
||||
if skipHookPrefix == "" {
|
||||
return gui.createErrorPanel(gui.Tr.SkipHookPrefixNotConfigured)
|
||||
}
|
||||
@@ -349,7 +345,7 @@ func (gui *Gui) handleWIPCommitPress() error {
|
||||
}
|
||||
|
||||
func (gui *Gui) commitPrefixConfigForRepo() *config.CommitPrefixConfig {
|
||||
cfg, ok := gui.Config.GetUserConfig().Git.CommitPrefixes[utils.GetCurrentRepoName()]
|
||||
cfg, ok := gui.UserConfig.Git.CommitPrefixes[utils.GetCurrentRepoName()]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
@@ -359,7 +355,7 @@ func (gui *Gui) commitPrefixConfigForRepo() *config.CommitPrefixConfig {
|
||||
|
||||
func (gui *Gui) prepareFilesForCommit() error {
|
||||
noStagedFiles := len(gui.stagedFiles()) == 0
|
||||
if noStagedFiles && gui.Config.GetUserConfig().Gui.SkipNoStagedFilesWarning {
|
||||
if noStagedFiles && gui.UserConfig.Gui.SkipNoStagedFilesWarning {
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.StageAllFiles).StageAll()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -393,10 +389,9 @@ func (gui *Gui) handleCommitPress() error {
|
||||
return gui.createErrorPanel(fmt.Sprintf("%s: %s", gui.Tr.LcCommitPrefixPatternError, err.Error()))
|
||||
}
|
||||
prefix := rgx.ReplaceAllString(gui.getCheckedOutBranch().Name, prefixReplace)
|
||||
gui.renderString(gui.Views.CommitMessage, prefix)
|
||||
if err := gui.Views.CommitMessage.SetCursor(len(prefix), 0); err != nil {
|
||||
return err
|
||||
}
|
||||
gui.Views.CommitMessage.ClearTextArea()
|
||||
gui.Views.CommitMessage.TextArea.TypeString(prefix)
|
||||
gui.render()
|
||||
}
|
||||
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
@@ -444,9 +439,9 @@ func (gui *Gui) handleAmendCommitPress() error {
|
||||
title: strings.Title(gui.Tr.AmendLastCommit),
|
||||
prompt: gui.Tr.SureToAmend,
|
||||
handleConfirm: func() error {
|
||||
cmdStr := gui.GitCommand.AmendHeadCmdStr()
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(cmdStr, gui.Tr.Spans.AmendCommit, true))
|
||||
return gui.withGpgHandling(cmdStr, gui.Tr.AmendingStatus, nil)
|
||||
cmdObj := gui.GitCommand.AmendHeadCmdObj()
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(cmdObj.ToString(), gui.Tr.Spans.AmendCommit, true))
|
||||
return gui.withGpgHandling(cmdObj, gui.Tr.AmendingStatus, nil)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -462,8 +457,16 @@ func (gui *Gui) handleCommitEditorPress() error {
|
||||
return gui.promptToStageAllAndRetry(gui.handleCommitEditorPress)
|
||||
}
|
||||
|
||||
args := []string{"commit"}
|
||||
|
||||
if gui.UserConfig.Git.Commit.SignOff {
|
||||
args = append(args, "--signoff")
|
||||
}
|
||||
|
||||
cmdStr := "git " + strings.Join(args, " ")
|
||||
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(
|
||||
gui.OSCommand.WithSpan(gui.Tr.Spans.Commit).PrepareSubProcess("git", "commit"),
|
||||
gui.GitCommand.WithSpan(gui.Tr.Spans.Commit).Cmd.New(cmdStr).Log(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -509,7 +512,7 @@ func (gui *Gui) editFileAtLine(filename string, lineNumber int) error {
|
||||
}
|
||||
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(
|
||||
gui.OSCommand.WithSpan(gui.Tr.Spans.EditFile).ShellCommandFromString(cmdStr),
|
||||
gui.OSCommand.WithSpan(gui.Tr.Spans.EditFile).Cmd.NewShell(cmdStr),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -551,7 +554,9 @@ func (gui *Gui) refreshStateFiles() error {
|
||||
prevNodes := gui.State.FileManager.GetAllItems()
|
||||
prevSelectedLineIdx := gui.State.Panels.Files.SelectedLineIdx
|
||||
|
||||
files := gui.GitCommand.GetStatusFiles(commands.GetStatusFileOptions{})
|
||||
files := loaders.
|
||||
NewFileLoader(gui.Common, gui.GitCommand.Cmd, gui.GitCommand.GitConfig).
|
||||
GetStatusFiles(loaders.GetStatusFileOptions{})
|
||||
|
||||
// for when you stage the old file of a rename and the new file is in a collapsed dir
|
||||
state.FileManager.RWMutex.Lock()
|
||||
@@ -658,9 +663,11 @@ func (gui *Gui) handlePullFiles() error {
|
||||
}
|
||||
}
|
||||
|
||||
suggestedRemote := getSuggestedRemote(gui.State.Remotes)
|
||||
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.EnterUpstream,
|
||||
initialContent: "origin/" + currentBranch.Name,
|
||||
initialContent: suggestedRemote + "/" + currentBranch.Name,
|
||||
findSuggestionsFunc: gui.getRemoteBranchesSuggestionsFunc("/"),
|
||||
handleConfirm: func(upstream string) error {
|
||||
if err := gui.GitCommand.SetUpstreamBranch(upstream); err != nil {
|
||||
@@ -729,15 +736,14 @@ func (gui *Gui) push(opts pushOpts) error {
|
||||
}
|
||||
go utils.Safe(func() {
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.Push).Push(commands.PushOpts{
|
||||
Force: opts.force,
|
||||
UpstreamRemote: opts.upstreamRemote,
|
||||
UpstreamBranch: opts.upstreamBranch,
|
||||
SetUpstream: opts.setUpstream,
|
||||
PromptUserForCredential: gui.promptUserForCredential,
|
||||
})
|
||||
Force: opts.force,
|
||||
UpstreamRemote: opts.upstreamRemote,
|
||||
UpstreamBranch: opts.upstreamBranch,
|
||||
SetUpstream: opts.setUpstream,
|
||||
}, gui.promptUserForCredential)
|
||||
|
||||
if err != nil && !opts.force && strings.Contains(err.Error(), "Updates were rejected") {
|
||||
forcePushDisabled := gui.Config.GetUserConfig().Git.DisableForcePushing
|
||||
forcePushDisabled := gui.UserConfig.Git.DisableForcePushing
|
||||
if forcePushDisabled {
|
||||
_ = gui.createErrorPanel(gui.Tr.UpdatesRejectedAndForcePushDisabled)
|
||||
return
|
||||
@@ -795,12 +801,14 @@ func (gui *Gui) pushFiles() error {
|
||||
)
|
||||
}
|
||||
|
||||
suggestedRemote := getSuggestedRemote(gui.State.Remotes)
|
||||
|
||||
if gui.GitCommand.PushToCurrent {
|
||||
return gui.push(pushOpts{setUpstream: true})
|
||||
} else {
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.EnterUpstream,
|
||||
initialContent: "origin " + currentBranch.Name,
|
||||
initialContent: suggestedRemote + " " + currentBranch.Name,
|
||||
findSuggestionsFunc: gui.getRemoteBranchesSuggestionsFunc(" "),
|
||||
handleConfirm: func(upstream string) error {
|
||||
var upstreamBranch, upstreamRemote string
|
||||
@@ -825,8 +833,22 @@ func (gui *Gui) pushFiles() error {
|
||||
}
|
||||
}
|
||||
|
||||
func getSuggestedRemote(remotes []*models.Remote) string {
|
||||
if len(remotes) == 0 {
|
||||
return "origin"
|
||||
}
|
||||
|
||||
for _, remote := range remotes {
|
||||
if remote.Name == "origin" {
|
||||
return remote.Name
|
||||
}
|
||||
}
|
||||
|
||||
return remotes[0].Name
|
||||
}
|
||||
|
||||
func (gui *Gui) requestToForcePush() error {
|
||||
forcePushDisabled := gui.Config.GetUserConfig().Git.DisableForcePushing
|
||||
forcePushDisabled := gui.UserConfig.Git.DisableForcePushing
|
||||
if forcePushDisabled {
|
||||
return gui.createErrorPanel(gui.Tr.ForcePushDisabled)
|
||||
}
|
||||
@@ -903,7 +925,7 @@ func (gui *Gui) handleCustomCommand() error {
|
||||
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(command, gui.Tr.Spans.CustomCommand, true))
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(
|
||||
gui.OSCommand.PrepareShellSubProcess(command),
|
||||
gui.OSCommand.Cmd.NewShell(command),
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -984,7 +1006,7 @@ func (gui *Gui) handleOpenMergeTool() error {
|
||||
prompt: gui.Tr.MergeToolPrompt,
|
||||
handleConfirm: func() error {
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(
|
||||
gui.OSCommand.ExecutableFromString(gui.GitCommand.OpenMergeToolCmd()),
|
||||
gui.GitCommand.OpenMergeToolCmdObj(),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetSuggestedRemote(t *testing.T) {
|
||||
cases := []struct {
|
||||
remotes []*models.Remote
|
||||
expected string
|
||||
}{
|
||||
{mkRemoteList(), "origin"},
|
||||
{mkRemoteList("upstream", "origin", "foo"), "origin"},
|
||||
{mkRemoteList("upstream", "foo", "bar"), "upstream"},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
result := getSuggestedRemote(c.remotes)
|
||||
assert.EqualValues(t, c.expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func mkRemoteList(names ...string) []*models.Remote {
|
||||
var result []*models.Remote
|
||||
|
||||
for _, name := range names {
|
||||
result = append(result, &models.Remote{Name: name})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -9,9 +9,11 @@ import (
|
||||
"github.com/xo/terminfo"
|
||||
)
|
||||
|
||||
func TestRender(t *testing.T) {
|
||||
func init() {
|
||||
color.ForceSetColorLevel(terminfo.ColorLevelNone)
|
||||
}
|
||||
|
||||
func TestRender(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
root *FileNode
|
||||
|
||||
+3
-3
@@ -32,7 +32,7 @@ func (gui *Gui) gitFlowFinishBranch(gitFlowConfig string, branchName string) err
|
||||
}
|
||||
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(
|
||||
gui.OSCommand.WithSpan(gui.Tr.Spans.GitFlowFinish).PrepareSubProcess("git", "flow", branchType, "finish", suffix),
|
||||
gui.GitCommand.WithSpan(gui.Tr.Spans.GitFlowFinish).Cmd.New("git flow " + branchType + " finish " + suffix).Log(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ func (gui *Gui) handleCreateGitFlowMenu() error {
|
||||
}
|
||||
|
||||
// get config
|
||||
gitFlowConfig, err := gui.GitCommand.RunCommandWithOutput("git config --local --get-regexp gitflow")
|
||||
gitFlowConfig, err := gui.GitCommand.Cmd.New("git config --local --get-regexp gitflow").RunWithOutput()
|
||||
if err != nil {
|
||||
return gui.createErrorPanel("You need to install git-flow and enable it in this repo to use git-flow features")
|
||||
}
|
||||
@@ -56,7 +56,7 @@ func (gui *Gui) handleCreateGitFlowMenu() error {
|
||||
title: title,
|
||||
handleConfirm: func(name string) error {
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(
|
||||
gui.OSCommand.WithSpan(gui.Tr.Spans.GitFlowStart).PrepareSubProcess("git", "flow", branchType, "start", name),
|
||||
gui.GitCommand.WithSpan(gui.Tr.Spans.GitFlowStart).Cmd.New("git flow " + branchType + " start " + name).Log(),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
const HORIZONTAL_SCROLL_FACTOR = 3
|
||||
|
||||
// these views need to be re-rendered when the screen mode changes. The commits view,
|
||||
// for example, will show authorship information in half and full screen mode.
|
||||
func (gui *Gui) rerenderViewsWithScreenModeDependentContent() error {
|
||||
@@ -62,7 +64,7 @@ func (gui *Gui) prevScreenMode() error {
|
||||
|
||||
func (gui *Gui) scrollUpView(view *gocui.View) error {
|
||||
ox, oy := view.Origin()
|
||||
newOy := int(math.Max(0, float64(oy-gui.Config.GetUserConfig().Gui.ScrollHeight)))
|
||||
newOy := int(math.Max(0, float64(oy-gui.UserConfig.Gui.ScrollHeight)))
|
||||
return view.SetOrigin(ox, newOy)
|
||||
}
|
||||
|
||||
@@ -84,12 +86,12 @@ func (gui *Gui) scrollDownView(view *gocui.View) error {
|
||||
func (gui *Gui) linesToScrollDown(view *gocui.View) int {
|
||||
_, oy := view.Origin()
|
||||
y := oy
|
||||
canScrollPastBottom := gui.Config.GetUserConfig().Gui.ScrollPastBottom
|
||||
canScrollPastBottom := gui.UserConfig.Gui.ScrollPastBottom
|
||||
if !canScrollPastBottom {
|
||||
_, sy := view.Size()
|
||||
y += sy
|
||||
}
|
||||
scrollHeight := gui.Config.GetUserConfig().Gui.ScrollHeight
|
||||
scrollHeight := gui.UserConfig.Gui.ScrollHeight
|
||||
scrollableLines := view.ViewLinesHeight() - y
|
||||
if scrollableLines < 0 {
|
||||
return 0
|
||||
@@ -114,7 +116,7 @@ func (gui *Gui) linesToScrollDown(view *gocui.View) int {
|
||||
|
||||
func (gui *Gui) scrollUpMain() error {
|
||||
if gui.canScrollMergePanel() {
|
||||
gui.State.Panels.Merging.UserScrolling = true
|
||||
gui.State.Panels.Merging.UserVerticalScrolling = true
|
||||
}
|
||||
|
||||
return gui.scrollUpView(gui.Views.Main)
|
||||
@@ -122,12 +124,33 @@ func (gui *Gui) scrollUpMain() error {
|
||||
|
||||
func (gui *Gui) scrollDownMain() error {
|
||||
if gui.canScrollMergePanel() {
|
||||
gui.State.Panels.Merging.UserScrolling = true
|
||||
gui.State.Panels.Merging.UserVerticalScrolling = true
|
||||
}
|
||||
|
||||
return gui.scrollDownView(gui.Views.Main)
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollLeftMain() error {
|
||||
gui.scrollLeft(gui.Views.Main)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollRightMain() error {
|
||||
gui.scrollRight(gui.Views.Main)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollLeft(view *gocui.View) {
|
||||
newOriginX := utils.Max(view.OriginX()-view.InnerWidth()/HORIZONTAL_SCROLL_FACTOR, 0)
|
||||
_ = view.SetOriginX(newOriginX)
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollRight(view *gocui.View) {
|
||||
_ = view.SetOriginX(view.OriginX() + view.InnerWidth()/HORIZONTAL_SCROLL_FACTOR)
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollUpSecondary() error {
|
||||
return gui.scrollUpView(gui.Views.Secondary)
|
||||
}
|
||||
@@ -166,9 +189,9 @@ func (gui *Gui) handleMouseDownMain() error {
|
||||
// set filename, set primary/secondary selected, set line number, then switch context
|
||||
// I'll need to know it was changed though.
|
||||
// Could I pass something along to the context change?
|
||||
return gui.enterFile(false, gui.Views.Main.SelectedLineIdx())
|
||||
return gui.enterFile(OnFocusOpts{ClickedViewName: "main", ClickedViewLineIdx: gui.Views.Main.SelectedLineIdx()})
|
||||
case gui.State.Contexts.CommitFiles:
|
||||
return gui.enterCommitFile(gui.Views.Main.SelectedLineIdx())
|
||||
return gui.enterCommitFile(OnFocusOpts{ClickedViewName: "main", ClickedViewLineIdx: gui.Views.Main.SelectedLineIdx()})
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -181,7 +204,7 @@ func (gui *Gui) handleMouseDownSecondary() error {
|
||||
|
||||
switch gui.g.CurrentView() {
|
||||
case gui.Views.Files:
|
||||
return gui.enterFile(true, gui.Views.Secondary.SelectedLineIdx())
|
||||
return gui.enterFile(OnFocusOpts{ClickedViewName: "secondary", ClickedViewLineIdx: gui.Views.Secondary.SelectedLineIdx()})
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
+42
-20
@@ -1,15 +1,21 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
)
|
||||
|
||||
// Currently there is a bug where if we switch to a subprocess from within
|
||||
// WithWaitingStatus we get stuck there and can't return to lazygit. We could
|
||||
// fix this bug, or just stop running subprocesses from within there, given that
|
||||
// we don't need to see a loading status if we're in a subprocess.
|
||||
func (gui *Gui) withGpgHandling(cmdStr string, waitingStatus string, onSuccess func() error) error {
|
||||
// TODO: work out if we actually need to use a shell command here
|
||||
func (gui *Gui) withGpgHandling(cmdObj oscommands.ICmdObj, waitingStatus string, onSuccess func() error) error {
|
||||
useSubprocess := gui.GitCommand.UsingGpg()
|
||||
if useSubprocess {
|
||||
// Need to remember why we use the shell for the subprocess but not in the other case
|
||||
// Maybe there's no good reason
|
||||
success, err := gui.runSubprocessWithSuspense(gui.OSCommand.ShellCommandFromString(cmdStr))
|
||||
success, err := gui.runSubprocessWithSuspense(gui.OSCommand.Cmd.NewShell(cmdObj.ToString()))
|
||||
if success && onSuccess != nil {
|
||||
if err := onSuccess(); err != nil {
|
||||
return err
|
||||
@@ -19,23 +25,39 @@ func (gui *Gui) withGpgHandling(cmdStr string, waitingStatus string, onSuccess f
|
||||
return err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
} else {
|
||||
return gui.WithWaitingStatus(waitingStatus, func() error {
|
||||
err := gui.OSCommand.RunCommand(cmdStr)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if onSuccess != nil {
|
||||
if err := onSuccess(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
})
|
||||
return gui.RunAndStream(cmdObj, waitingStatus, onSuccess)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
func (gui *Gui) RunAndStream(cmdObj oscommands.ICmdObj, waitingStatus string, onSuccess func() error) error {
|
||||
return gui.WithWaitingStatus(waitingStatus, func() error {
|
||||
cmdObj := gui.OSCommand.Cmd.NewShell(cmdObj.ToString())
|
||||
cmdObj.AddEnvVars("TERM=dumb")
|
||||
cmdWriter := gui.getCmdWriter()
|
||||
cmd := cmdObj.GetCmd()
|
||||
cmd.Stdout = cmdWriter
|
||||
cmd.Stderr = cmdWriter
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
if _, err := cmd.Stdout.Write([]byte(fmt.Sprintf("%s\n", style.FgRed.Sprint(err.Error())))); err != nil {
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
return gui.surfaceError(
|
||||
fmt.Errorf(
|
||||
gui.Tr.GitCommandFailed, gui.UserConfig.Keybinding.Universal.ExtrasMenu,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if onSuccess != nil {
|
||||
if err := onSuccess(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
})
|
||||
}
|
||||
|
||||
+44
-24
@@ -7,7 +7,6 @@ import (
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -15,6 +14,7 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/lbl"
|
||||
@@ -22,14 +22,14 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/modes/cherrypicking"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/modes/diffing"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/modes/filtering"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation/authors"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation/graph"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/tasks"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/updates"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/ozeidan/fuzzy-patricia.v3/patricia"
|
||||
)
|
||||
|
||||
@@ -66,8 +66,8 @@ type Repo string
|
||||
|
||||
// Gui wraps the gocui Gui object which handles rendering and events
|
||||
type Gui struct {
|
||||
*common.Common
|
||||
g *gocui.Gui
|
||||
Log *logrus.Entry
|
||||
GitCommand *commands.GitCommand
|
||||
OSCommand *oscommands.OSCommand
|
||||
|
||||
@@ -78,7 +78,6 @@ type Gui struct {
|
||||
// gui state when returning from a subrepo
|
||||
RepoStateMap map[Repo]*guiState
|
||||
Config config.AppConfigurer
|
||||
Tr *i18n.TranslationSet
|
||||
Updater *updates.Updater
|
||||
statusManager *statusManager
|
||||
credentials credentials
|
||||
@@ -119,6 +118,10 @@ type Gui struct {
|
||||
ShowExtrasWindow bool
|
||||
|
||||
suggestionsAsyncHandler *tasks.AsyncHandler
|
||||
|
||||
PopupHandler PopupHandler
|
||||
|
||||
IsNewRepo bool
|
||||
}
|
||||
|
||||
type listPanelState struct {
|
||||
@@ -144,9 +147,9 @@ type LblPanelState struct {
|
||||
type MergingPanelState struct {
|
||||
*mergeconflicts.State
|
||||
|
||||
// UserScrolling tells us if the user has started scrolling through the file themselves
|
||||
// UserVerticalScrolling tells us if the user has started scrolling through the file themselves
|
||||
// in which case we won't auto-scroll to a conflict.
|
||||
UserScrolling bool
|
||||
UserVerticalScrolling bool
|
||||
}
|
||||
|
||||
type filePanelState struct {
|
||||
@@ -313,6 +316,8 @@ type guiState struct {
|
||||
RetainOriginalDir bool
|
||||
IsRefreshingFiles bool
|
||||
Searching searchingState
|
||||
// if this is true, we'll load our commits using `git log --all`
|
||||
ShowWholeGitGraph bool
|
||||
ScreenMode WindowMaximisation
|
||||
Ptmx *os.File
|
||||
PrevMainWidth int
|
||||
@@ -368,7 +373,7 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
|
||||
}
|
||||
}
|
||||
|
||||
showTree := gui.Config.GetUserConfig().Gui.ShowFileTree
|
||||
showTree := gui.UserConfig.Gui.ShowFileTree
|
||||
|
||||
contexts := gui.contextTree()
|
||||
|
||||
@@ -394,7 +399,7 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
|
||||
Remotes: &remotePanelState{listPanelState{SelectedLineIdx: 0}},
|
||||
RemoteBranches: &remoteBranchesState{listPanelState{SelectedLineIdx: -1}},
|
||||
Tags: &tagsPanelState{listPanelState{SelectedLineIdx: -1}},
|
||||
Commits: &commitPanelState{listPanelState: listPanelState{SelectedLineIdx: -1}, LimitCommits: true},
|
||||
Commits: &commitPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}, LimitCommits: true},
|
||||
ReflogCommits: &reflogCommitPanelState{listPanelState{SelectedLineIdx: 0}},
|
||||
SubCommits: &subCommitPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}, refName: ""},
|
||||
CommitFiles: &commitFilesPanelState{listPanelState: listPanelState{SelectedLineIdx: -1}, refName: ""},
|
||||
@@ -402,8 +407,8 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
|
||||
Menu: &menuPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}, OnPress: nil},
|
||||
Suggestions: &suggestionsPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}},
|
||||
Merging: &MergingPanelState{
|
||||
State: mergeconflicts.NewState(),
|
||||
UserScrolling: false,
|
||||
State: mergeconflicts.NewState(),
|
||||
UserVerticalScrolling: false,
|
||||
},
|
||||
},
|
||||
Ptmx: nil,
|
||||
@@ -426,13 +431,13 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
|
||||
|
||||
// for now the split view will always be on
|
||||
// NewGui builds a new gui handler
|
||||
func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *oscommands.OSCommand, tr *i18n.TranslationSet, config config.AppConfigurer, updater *updates.Updater, filterPath string, showRecentRepos bool) (*Gui, error) {
|
||||
func NewGui(cmn *common.Common, gitCommand *commands.GitCommand, oSCommand *oscommands.OSCommand, config config.AppConfigurer, updater *updates.Updater, filterPath string, showRecentRepos bool) (*Gui, error) {
|
||||
|
||||
gui := &Gui{
|
||||
Log: log,
|
||||
Common: cmn,
|
||||
GitCommand: gitCommand,
|
||||
OSCommand: oSCommand,
|
||||
Config: config,
|
||||
Tr: tr,
|
||||
Updater: updater,
|
||||
statusManager: &statusManager{},
|
||||
viewBufferManagerMap: map[string]*tasks.ViewBufferManager{},
|
||||
@@ -440,8 +445,12 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *oscom
|
||||
RepoPathStack: []string{},
|
||||
RepoStateMap: map[Repo]*guiState{},
|
||||
CmdLog: []string{},
|
||||
ShowExtrasWindow: config.ShowCommandLogOnStartup(),
|
||||
suggestionsAsyncHandler: tasks.NewAsyncHandler(),
|
||||
|
||||
// originally we could only hide the command log permanently via the config
|
||||
// but now we do it via state. So we need to still support the config for the
|
||||
// sake of backwards compatibility. We're making use of short circuiting here
|
||||
ShowExtrasWindow: cmn.UserConfig.Gui.ShowCommandLog && !config.GetAppState().HideCommandLog,
|
||||
}
|
||||
|
||||
gui.resetState(filterPath, false)
|
||||
@@ -451,10 +460,19 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *oscom
|
||||
onRunCommand := gui.GetOnRunCommand()
|
||||
oSCommand.SetOnRunCommand(onRunCommand)
|
||||
gui.OnRunCommand = onRunCommand
|
||||
gui.PopupHandler = &RealPopupHandler{gui: gui}
|
||||
|
||||
authors.SetCustomAuthors(gui.UserConfig.Gui.AuthorColors)
|
||||
|
||||
return gui, nil
|
||||
}
|
||||
|
||||
var RuneReplacements = map[rune]string{
|
||||
// for the commit graph
|
||||
graph.MergeSymbol: "M",
|
||||
graph.CommitSymbol: "o",
|
||||
}
|
||||
|
||||
// Run setup the gui with keybindings and start the mainloop
|
||||
func (gui *Gui) Run() error {
|
||||
recordEvents := recordingEvents()
|
||||
@@ -465,7 +483,7 @@ func (gui *Gui) Run() error {
|
||||
playMode = gocui.REPLAYING
|
||||
}
|
||||
|
||||
g, err := gocui.NewGui(gocui.OutputTrue, OverlappingEdges, playMode, headless())
|
||||
g, err := gocui.NewGui(gocui.OutputTrue, OverlappingEdges, playMode, headless(), RuneReplacements)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -494,7 +512,7 @@ func (gui *Gui) Run() error {
|
||||
if err := gui.Config.ReloadUserConfig(); err != nil {
|
||||
return nil
|
||||
}
|
||||
userConfig := gui.Config.GetUserConfig()
|
||||
userConfig := gui.UserConfig
|
||||
g.SearchEscapeKey = gui.getKey(userConfig.Keybinding.Universal.Return)
|
||||
g.NextSearchMatchKey = gui.getKey(userConfig.Keybinding.Universal.NextMatch)
|
||||
g.PrevSearchMatchKey = gui.getKey(userConfig.Keybinding.Universal.PrevMatch)
|
||||
@@ -510,7 +528,7 @@ func (gui *Gui) Run() error {
|
||||
}
|
||||
|
||||
gui.waitForIntro.Add(1)
|
||||
if gui.Config.GetUserConfig().Git.AutoFetch {
|
||||
if gui.UserConfig.Git.AutoFetch {
|
||||
go utils.Safe(gui.startBackgroundFetch)
|
||||
}
|
||||
|
||||
@@ -563,7 +581,7 @@ func (gui *Gui) RunAndHandleError() error {
|
||||
}
|
||||
|
||||
// returns whether command exited without error or not
|
||||
func (gui *Gui) runSubprocessWithSuspenseAndRefresh(subprocess *exec.Cmd) error {
|
||||
func (gui *Gui) runSubprocessWithSuspenseAndRefresh(subprocess oscommands.ICmdObj) error {
|
||||
_, err := gui.runSubprocessWithSuspense(subprocess)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -577,7 +595,7 @@ func (gui *Gui) runSubprocessWithSuspenseAndRefresh(subprocess *exec.Cmd) error
|
||||
}
|
||||
|
||||
// returns whether command exited without error or not
|
||||
func (gui *Gui) runSubprocessWithSuspense(subprocess *exec.Cmd) (bool, error) {
|
||||
func (gui *Gui) runSubprocessWithSuspense(subprocess oscommands.ICmdObj) (bool, error) {
|
||||
gui.Mutexes.SubprocessMutex.Lock()
|
||||
defer gui.Mutexes.SubprocessMutex.Unlock()
|
||||
|
||||
@@ -606,7 +624,8 @@ func (gui *Gui) runSubprocessWithSuspense(subprocess *exec.Cmd) (bool, error) {
|
||||
return cmdErr == nil, gui.surfaceError(cmdErr)
|
||||
}
|
||||
|
||||
func (gui *Gui) runSubprocess(subprocess *exec.Cmd) error {
|
||||
func (gui *Gui) runSubprocess(cmdObj oscommands.ICmdObj) error {
|
||||
subprocess := cmdObj.GetCmd()
|
||||
subprocess.Stdout = os.Stdout
|
||||
subprocess.Stderr = os.Stdout
|
||||
subprocess.Stdin = os.Stdin
|
||||
@@ -695,8 +714,8 @@ func (gui *Gui) goEvery(interval time.Duration, stop chan struct{}, function fun
|
||||
|
||||
func (gui *Gui) startBackgroundFetch() {
|
||||
gui.waitForIntro.Wait()
|
||||
isNew := gui.Config.GetIsNewRepo()
|
||||
userConfig := gui.Config.GetUserConfig()
|
||||
isNew := gui.IsNewRepo
|
||||
userConfig := gui.UserConfig
|
||||
if !isNew {
|
||||
time.After(time.Duration(userConfig.Refresher.FetchInterval) * time.Second)
|
||||
}
|
||||
@@ -709,6 +728,7 @@ func (gui *Gui) startBackgroundFetch() {
|
||||
} else {
|
||||
gui.goEvery(time.Second*time.Duration(userConfig.Refresher.FetchInterval), gui.stopChan, func() error {
|
||||
err := gui.fetch(false, "")
|
||||
gui.render()
|
||||
return err
|
||||
})
|
||||
}
|
||||
@@ -716,7 +736,7 @@ func (gui *Gui) startBackgroundFetch() {
|
||||
|
||||
// setColorScheme sets the color scheme for the app based on the user config
|
||||
func (gui *Gui) setColorScheme() error {
|
||||
userConfig := gui.Config.GetUserConfig()
|
||||
userConfig := gui.UserConfig
|
||||
theme.UpdateTheme(userConfig.Gui.Theme)
|
||||
|
||||
gui.g.FgColor = theme.InactiveBorderColor
|
||||
|
||||
+42
-1
@@ -204,7 +204,7 @@ func (gui *Gui) getKey(key string) interface{} {
|
||||
|
||||
// GetInitialKeybindings is a function.
|
||||
func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
config := gui.Config.GetUserConfig().Keybinding
|
||||
config := gui.UserConfig.Keybinding
|
||||
|
||||
bindings := []*Binding{
|
||||
{
|
||||
@@ -724,6 +724,14 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
Handler: gui.handleFetchRemote,
|
||||
Description: gui.Tr.LcFetchRemote,
|
||||
},
|
||||
{
|
||||
ViewName: "commits",
|
||||
Contexts: []string{string(BRANCH_COMMITS_CONTEXT_KEY)},
|
||||
Key: gui.getKey(config.Commits.OpenLogMenu),
|
||||
Handler: gui.handleOpenLogMenu,
|
||||
Description: gui.Tr.LcOpenLogMenu,
|
||||
OpensMenu: true,
|
||||
},
|
||||
{
|
||||
ViewName: "commits",
|
||||
Contexts: []string{string(BRANCH_COMMITS_CONTEXT_KEY)},
|
||||
@@ -893,6 +901,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
Handler: gui.handleCopySelectedCommitMessageToClipboard,
|
||||
Description: gui.Tr.LcCopyCommitMessageToClipboard,
|
||||
},
|
||||
{
|
||||
ViewName: "commits",
|
||||
Contexts: []string{string(BRANCH_COMMITS_CONTEXT_KEY)},
|
||||
Key: gui.getKey(config.Commits.OpenInBrowser),
|
||||
Handler: gui.handleOpenCommitInBrowser,
|
||||
Description: gui.Tr.LcOpenCommitInBrowser,
|
||||
},
|
||||
{
|
||||
ViewName: "commits",
|
||||
Contexts: []string{string(REFLOG_COMMITS_CONTEXT_KEY)},
|
||||
@@ -1426,6 +1441,20 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.scrollDownMain,
|
||||
},
|
||||
{
|
||||
ViewName: "main",
|
||||
Contexts: []string{string(MAIN_PATCH_BUILDING_CONTEXT_KEY), string(MAIN_STAGING_CONTEXT_KEY), string(MAIN_MERGING_CONTEXT_KEY)},
|
||||
Key: gui.getKey(config.Universal.ScrollLeft),
|
||||
Handler: gui.scrollLeftMain,
|
||||
Description: gui.Tr.LcScrollLeft,
|
||||
},
|
||||
{
|
||||
ViewName: "main",
|
||||
Contexts: []string{string(MAIN_PATCH_BUILDING_CONTEXT_KEY), string(MAIN_STAGING_CONTEXT_KEY), string(MAIN_MERGING_CONTEXT_KEY)},
|
||||
Key: gui.getKey(config.Universal.ScrollRight),
|
||||
Handler: gui.scrollRightMain,
|
||||
Description: gui.Tr.LcScrollRight,
|
||||
},
|
||||
{
|
||||
ViewName: "main",
|
||||
Contexts: []string{string(MAIN_STAGING_CONTEXT_KEY)},
|
||||
@@ -1748,6 +1777,18 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
Handler: gui.toggleWhitespaceInDiffView,
|
||||
Description: gui.Tr.ToggleWhitespaceInDiffView,
|
||||
},
|
||||
{
|
||||
ViewName: "",
|
||||
Key: gui.getKey(config.Universal.IncreaseContextInDiffView),
|
||||
Handler: gui.IncreaseContextInDiffView,
|
||||
Description: gui.Tr.IncreaseContextInDiffView,
|
||||
},
|
||||
{
|
||||
ViewName: "",
|
||||
Key: gui.getKey(config.Universal.DecreaseContextInDiffView),
|
||||
Handler: gui.DecreaseContextInDiffView,
|
||||
Description: gui.Tr.DecreaseContextInDiffView,
|
||||
},
|
||||
{
|
||||
ViewName: "extras",
|
||||
Key: gocui.MouseWheelUp,
|
||||
|
||||
+6
-10
@@ -52,24 +52,19 @@ func (gui *Gui) createAllViews() error {
|
||||
|
||||
gui.Views.Stash.Title = gui.Tr.StashTitle
|
||||
gui.Views.Stash.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.Stash.ContainsList = true
|
||||
|
||||
gui.Views.Commits.Title = gui.Tr.CommitsTitle
|
||||
gui.Views.Commits.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.Commits.ContainsList = true
|
||||
|
||||
gui.Views.CommitFiles.Title = gui.Tr.CommitFiles
|
||||
gui.Views.CommitFiles.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.CommitFiles.ContainsList = true
|
||||
|
||||
gui.Views.Branches.Title = gui.Tr.BranchesTitle
|
||||
gui.Views.Branches.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.Branches.ContainsList = true
|
||||
|
||||
gui.Views.Files.Highlight = true
|
||||
gui.Views.Files.Title = gui.Tr.FilesTitle
|
||||
gui.Views.Files.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.Files.ContainsList = true
|
||||
|
||||
gui.Views.Secondary.Title = gui.Tr.DiffTitle
|
||||
gui.Views.Secondary.Wrap = true
|
||||
@@ -111,7 +106,6 @@ func (gui *Gui) createAllViews() error {
|
||||
gui.Views.Credentials.Editable = true
|
||||
|
||||
gui.Views.Suggestions.Visible = false
|
||||
gui.Views.Suggestions.ContainsList = true
|
||||
|
||||
gui.Views.Menu.Visible = false
|
||||
|
||||
@@ -123,12 +117,15 @@ func (gui *Gui) createAllViews() error {
|
||||
gui.Views.Extras.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.Extras.Autoscroll = true
|
||||
gui.Views.Extras.Wrap = true
|
||||
|
||||
gui.printCommandLogHeader()
|
||||
|
||||
if _, err := gui.g.SetCurrentView(gui.defaultSideContext().GetViewName()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.GitCommand.GetCmdWriter = gui.getCmdWriter
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -261,7 +258,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
||||
}
|
||||
|
||||
for _, listContext := range gui.getListContexts() {
|
||||
view, err := gui.g.View(listContext.ViewName)
|
||||
view, err := gui.g.View(listContext.GetViewName())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -271,8 +268,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
||||
continue
|
||||
}
|
||||
|
||||
// check if the selected line is now out of view and if so refocus it
|
||||
view.FocusPoint(0, listContext.GetPanelState().GetSelectedLineIdx())
|
||||
listContext.FocusLine()
|
||||
|
||||
view.SelBgColor = theme.GocuiSelectedLineBgColor
|
||||
|
||||
@@ -378,7 +374,7 @@ func (gui *Gui) onInitialViewsCreation() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if !gui.Config.GetUserConfig().DisableStartupPopups {
|
||||
if !gui.UserConfig.DisableStartupPopups {
|
||||
popupTasks := []func(chan struct{}) error{}
|
||||
storedPopupVersion := gui.Config.GetAppState().StartupPopupVersion
|
||||
if storedPopupVersion < StartupPopupVersion {
|
||||
|
||||
@@ -174,7 +174,7 @@ func (gui *Gui) focusSelection(state *LblPanelState) error {
|
||||
newOrigin := state.CalculateOrigin(origin, bufferHeight)
|
||||
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
if err := stagingView.SetOrigin(0, newOrigin); err != nil {
|
||||
if err := stagingView.SetOriginY(newOrigin); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user