Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f91adf026b | ||
|
|
6d91661d5e | ||
|
|
90983aae65 | ||
|
|
f71b23b890 | ||
|
|
05a23f0e1e | ||
|
|
fd38ad8096 | ||
|
|
d502c43ae8 | ||
|
|
0df02dacc2 | ||
|
|
3258c24fb3 | ||
|
|
e7c657fba0 | ||
|
|
60468d2e17 | ||
|
|
cb78cf7de4 | ||
|
|
94b52af661 | ||
|
|
472288c81b | ||
|
|
258eedb38c | ||
|
|
bc044c64b2 | ||
|
|
e478c254d4 | ||
|
|
44f7fc6f7c | ||
|
|
f92fcfbb47 | ||
|
|
6ccf58c224 | ||
|
|
9190e9beac | ||
|
|
a99e6ba071 | ||
|
|
604ee02cd9 | ||
|
|
926a48a65b | ||
|
|
98375dc902 | ||
|
|
e73de332a1 | ||
|
|
b28b2d05bd | ||
|
|
9e5f031553 | ||
|
|
dab5ba363c | ||
|
|
e42387d0da | ||
|
|
730a03a3b2 | ||
|
|
7d195b97c2 | ||
|
|
4fb2dba587 | ||
|
|
76697280c9 | ||
|
|
0df6ac6140 | ||
|
|
5453b71fd1 | ||
|
|
1f3070c882 | ||
|
|
3b7e7a7f56 | ||
|
|
06a8eb115c | ||
|
|
e4f0a470e9 | ||
|
|
adee0b8ccb | ||
|
|
0bebfe454e | ||
|
|
84b0c3df4f | ||
|
|
764bd556f3 | ||
|
|
069c7c9d35 | ||
|
|
393ce05860 | ||
|
|
cf78b86cb5 | ||
|
|
4f03d7733a | ||
|
|
e3a14d546a | ||
|
|
f2007f4d95 | ||
|
|
8969464b00 | ||
|
|
6137d66914 | ||
|
|
6fbe660f96 | ||
|
|
74320f0075 | ||
|
|
bfad972f0c | ||
|
|
bb918b579a | ||
|
|
e145090046 | ||
|
|
70b5c822bb | ||
|
|
f2df77a4f1 | ||
|
|
8d416634ba | ||
|
|
9f4433d8b5 | ||
|
|
2d8f7d2a7b | ||
|
|
a9fbc9eda1 | ||
|
|
e092da5f78 | ||
|
|
e42e7e5cbd | ||
|
|
93fac1f312 | ||
|
|
d5504fa5d0 | ||
|
|
273aba38d4 | ||
|
|
cab0aa462c | ||
|
|
b03e2270a0 | ||
|
|
21049be233 | ||
|
|
f89c47b83d | ||
|
|
44f1f22068 | ||
|
|
a229547048 | ||
|
|
4f700c23ba | ||
|
|
b69fc19b35 |
23
.github/workflows/cd.yml
vendored
23
.github/workflows/cd.yml
vendored
@@ -29,26 +29,3 @@ jobs:
|
||||
with:
|
||||
token: ${{secrets.GITHUB_API_TOKEN}}
|
||||
formula: lazygit
|
||||
ppa:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout PPA repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: dawidd6/lazygit-debian
|
||||
token: ${{secrets.GITHUB_API_TOKEN}}
|
||||
fetch-depth: 0
|
||||
- name: Setup git
|
||||
uses: dawidd6/action-git-user-config@v1
|
||||
- name: Update PPA repo
|
||||
run: |
|
||||
version="$(echo "$GITHUB_REF" | sed 's@refs/tags/v@@')"
|
||||
sudo apt update
|
||||
sudo apt install -y git-buildpackage
|
||||
git fetch --tags https://github.com/$GITHUB_REPOSITORY
|
||||
gbp import-ref -u "$version"
|
||||
gbp dch -D xenial -N "$version"-1
|
||||
git add debian/changelog
|
||||
git commit -m "d/changelog: dch $version"
|
||||
gbp tag
|
||||
git push --tags origin master
|
||||
|
||||
@@ -21,7 +21,6 @@ If you're a mere mortal like me and you're tired of hearing how powerful git is
|
||||
- [Binary releases](#binary-releases)
|
||||
- [Homebrew](#homebrew)
|
||||
- [MacPorts](#macports)
|
||||
- [Ubuntu](#ubuntu)
|
||||
- [Void Linux](#void-linux)
|
||||
- [Scoop (Windows)](#scoop-windows)
|
||||
- [Arch Linux](#arch-linux)
|
||||
@@ -82,6 +81,8 @@ sudo port install lazygit
|
||||
|
||||
### Ubuntu
|
||||
|
||||
**Deprecated**: will no longer receive updates.
|
||||
|
||||
Packages for Ubuntu are available via [Launchpad PPA](https://launchpad.net/~lazygit-team).
|
||||
|
||||
```sh
|
||||
@@ -114,12 +115,12 @@ scoop install lazygit
|
||||
|
||||
### Arch Linux
|
||||
|
||||
Packages for Arch Linux are available via AUR (Arch User Repository).
|
||||
Packages for Arch Linux are available via pacman and AUR (Arch User Repository).
|
||||
|
||||
There are two packages. The stable one which is built with the latest release
|
||||
and the git version which builds from the most recent commit.
|
||||
|
||||
- Stable: <https://aur.archlinux.org/packages/lazygit/>
|
||||
- Stable: `sudo pacman -S lazygit`
|
||||
- Development: <https://aur.archlinux.org/packages/lazygit-git/>
|
||||
|
||||
Instruction of how to install AUR content can be found here:
|
||||
|
||||
499
docs/Config.md
499
docs/Config.md
@@ -2,183 +2,196 @@
|
||||
|
||||
Default path for the config file:
|
||||
|
||||
* Linux: `~/.config/lazygit/config.yml`
|
||||
* MacOS: `~/Library/Application Support/jesseduffield/lazygit/config.yml`
|
||||
* Windows: `%APPDATA%\jesseduffield\lazygit\config.yml`
|
||||
- Linux: `~/.config/lazygit/config.yml`
|
||||
- MacOS: `~/Library/Application Support/lazygit/config.yml`
|
||||
- Windows: `%APPDATA%\lazygit\config.yml`
|
||||
|
||||
For old installations (slightly embarrassing: I didn't realise at the time that you didn't need to supply a vendor name to the path so I just used my name):
|
||||
|
||||
- Linux: `~/.config/jesseduffield/lazygit/config.yml`
|
||||
- MacOS: `~/Library/Application Support/jesseduffield/lazygit/config.yml`
|
||||
- Windows: `%APPDATA%\jesseduffield\lazygit\config.yml`
|
||||
|
||||
## Default
|
||||
|
||||
```yaml
|
||||
gui:
|
||||
# stuff relating to the UI
|
||||
scrollHeight: 2 # how many lines you scroll by
|
||||
scrollPastBottom: true # enable scrolling past the bottom
|
||||
sidePanelWidth: 0.3333 # number from 0 to 1
|
||||
expandFocusedSidePanel: false
|
||||
mainPanelSplitMode: 'flexible' # one of 'horizontal' | 'flexible' | 'vertical'
|
||||
theme:
|
||||
lightTheme: false # For terminals with a light background
|
||||
activeBorderColor:
|
||||
- white
|
||||
- bold
|
||||
inactiveBorderColor:
|
||||
- green
|
||||
optionsTextColor:
|
||||
- blue
|
||||
selectedLineBgColor:
|
||||
- default
|
||||
selectedRangeBgColor:
|
||||
- blue
|
||||
commitLength:
|
||||
show: true
|
||||
mouseEvents: true
|
||||
skipUnstageLineWarning: false
|
||||
skipStashWarning: true
|
||||
showFileTree: false # for rendering changes files in a tree format
|
||||
git:
|
||||
paging:
|
||||
colorArg: always
|
||||
useConfig: false
|
||||
merging:
|
||||
# only applicable to unix users
|
||||
manualCommit: false
|
||||
# extra args passed to `git merge`, e.g. --no-ff
|
||||
args: ""
|
||||
pull:
|
||||
mode: 'merge' # one of 'merge' | 'rebase' | 'ff-only'
|
||||
skipHookPrefix: WIP
|
||||
autoFetch: true
|
||||
branchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --"
|
||||
allBranchesLogCmd: "git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium"
|
||||
overrideGpg: false # prevents lazygit from spawning a separate process when using GPG
|
||||
disableForcePushing: false
|
||||
refresher:
|
||||
refreshInterval: 10 # file/submodule refresh interval in seconds
|
||||
fetchInterval: 60 # re-fetch interval in seconds
|
||||
update:
|
||||
method: prompt # can be: prompt | background | never
|
||||
days: 14 # how often an update is checked for
|
||||
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
|
||||
confirmOnQuit: false
|
||||
# determines whether hitting 'esc' will quit the application when there is nothing to cancel/close
|
||||
quitOnTopLevelReturn: false
|
||||
disableStartupPopups: false
|
||||
notARepository: 'prompt' # one of: 'prompt' | 'create' | 'skip'
|
||||
keybinding:
|
||||
universal:
|
||||
quit: 'q'
|
||||
quit-alt1: '<c-c>' # alternative/alias of quit
|
||||
return: '<esc>' # return to previous menu, will quit if there's nowhere to return
|
||||
quitWithoutChangingDirectory: 'Q'
|
||||
togglePanel: '<tab>' # goto the next panel
|
||||
prevItem: '<up>' # go one line up
|
||||
nextItem: '<down>' # go one line down
|
||||
prevItem-alt: 'k' # go one line up
|
||||
nextItem-alt: 'j' # go one line down
|
||||
prevPage: ',' # go to next page in list
|
||||
nextPage: '.' # go to previous page in list
|
||||
gotoTop: '<' # go to top of list
|
||||
gotoBottom: '>' # go to bottom of list
|
||||
prevBlock: '<left>' # goto the previous block / panel
|
||||
nextBlock: '<right>' # goto the next block / panel
|
||||
prevBlock-alt: 'h' # goto the previous block / panel
|
||||
nextBlock-alt: 'l' # goto the next block / panel
|
||||
nextMatch: 'n'
|
||||
prevMatch: 'N'
|
||||
optionMenu: 'x' # show help menu
|
||||
optionMenu-alt1: '?' # show help menu
|
||||
select: '<space>'
|
||||
goInto: '<enter>'
|
||||
confirm: '<enter>'
|
||||
confirm-alt1: 'y'
|
||||
remove: 'd'
|
||||
new: 'n'
|
||||
edit: 'e'
|
||||
openFile: 'o'
|
||||
scrollUpMain: '<pgup>' # main panel scrool up
|
||||
scrollDownMain: '<pgdown>' # main panel scrool down
|
||||
scrollUpMain-alt1: 'K' # main panel scrool up
|
||||
scrollDownMain-alt1: 'J' # main panel scrool down
|
||||
scrollUpMain-alt2: '<c-u>' # main panel scrool up
|
||||
scrollDownMain-alt2: '<c-d>' # main panel scrool down
|
||||
executeCustomCommand: ':'
|
||||
createRebaseOptionsMenu: 'm'
|
||||
pushFiles: 'P'
|
||||
pullFiles: 'p'
|
||||
refresh: 'R'
|
||||
createPatchOptionsMenu: '<c-p>'
|
||||
nextTab: ']'
|
||||
prevTab: '['
|
||||
nextScreenMode: '+'
|
||||
prevScreenMode: '_'
|
||||
undo: 'z'
|
||||
redo: '<c-z>'
|
||||
filteringMenu: '<c-s>'
|
||||
diffingMenu: 'W'
|
||||
diffingMenu-alt: '<c-e>' # deprecated
|
||||
copyToClipboard: '<c-o>'
|
||||
submitEditorText: '<enter>'
|
||||
appendNewline: '<tab>'
|
||||
status:
|
||||
checkForUpdate: 'u'
|
||||
recentRepos: '<enter>'
|
||||
files:
|
||||
commitChanges: 'c'
|
||||
commitChangesWithoutHook: 'w' # commit changes without pre-commit hook
|
||||
amendLastCommit: 'A'
|
||||
commitChangesWithEditor: 'C'
|
||||
ignoreFile: 'i'
|
||||
refreshFiles: 'r'
|
||||
stashAllChanges: 's'
|
||||
viewStashOptions: 'S'
|
||||
toggleStagedAll: 'a' # stage/unstage all
|
||||
viewResetOptions: 'D'
|
||||
fetch: 'f'
|
||||
toggleTreeView: '`'
|
||||
branches:
|
||||
createPullRequest: 'o'
|
||||
checkoutBranchByName: 'c'
|
||||
forceCheckoutBranch: 'F'
|
||||
rebaseBranch: 'r'
|
||||
mergeIntoCurrentBranch: 'M'
|
||||
viewGitFlowOptions: 'i'
|
||||
fastForward: 'f' # fast-forward this branch from its upstream
|
||||
pushTag: 'P'
|
||||
setUpstream: 'u' # set as upstream of checked-out branch
|
||||
fetchRemote: 'f'
|
||||
commits:
|
||||
squashDown: 's'
|
||||
renameCommit: 'r'
|
||||
renameCommitWithEditor: 'R'
|
||||
viewResetOptions: 'g'
|
||||
markCommitAsFixup: 'f'
|
||||
createFixupCommit: 'F' # create fixup commit for this commit
|
||||
squashAboveCommits: 'S'
|
||||
moveDownCommit: '<c-j>' # move commit down one
|
||||
moveUpCommit: '<c-k>' # move commit up one
|
||||
amendToCommit: 'A'
|
||||
pickCommit: 'p' # pick commit (when mid-rebase)
|
||||
revertCommit: 't'
|
||||
cherryPickCopy: 'c'
|
||||
cherryPickCopyRange: 'C'
|
||||
pasteCommits: 'v'
|
||||
tagCommit: 'T'
|
||||
checkoutCommit: '<space>'
|
||||
resetCherryPick: '<c-R>'
|
||||
copyCommitMessageToClipboard: '<c-y>'
|
||||
stash:
|
||||
popStash: 'g'
|
||||
commitFiles:
|
||||
checkoutCommitFile: 'c'
|
||||
main:
|
||||
toggleDragSelect: 'v'
|
||||
toggleDragSelect-alt: 'V'
|
||||
toggleSelectHunk: 'a'
|
||||
pickBothHunks: 'b'
|
||||
submodules:
|
||||
init: 'i'
|
||||
update: 'u'
|
||||
bulkMenu: 'b'
|
||||
gui:
|
||||
# stuff relating to the UI
|
||||
scrollHeight: 2 # how many lines you scroll by
|
||||
scrollPastBottom: true # enable scrolling past the bottom
|
||||
sidePanelWidth: 0.3333 # number from 0 to 1
|
||||
expandFocusedSidePanel: false
|
||||
mainPanelSplitMode: 'flexible' # one of 'horizontal' | 'flexible' | 'vertical'
|
||||
theme:
|
||||
lightTheme: false # For terminals with a light background
|
||||
activeBorderColor:
|
||||
- white
|
||||
- bold
|
||||
inactiveBorderColor:
|
||||
- green
|
||||
optionsTextColor:
|
||||
- blue
|
||||
selectedLineBgColor:
|
||||
- default
|
||||
selectedRangeBgColor:
|
||||
- blue
|
||||
commitLength:
|
||||
show: true
|
||||
mouseEvents: true
|
||||
skipUnstageLineWarning: false
|
||||
skipStashWarning: true
|
||||
showFileTree: false # for rendering changes files in a tree format
|
||||
showRandomTip: true
|
||||
showCommandLog: true
|
||||
commandLogSize: 8
|
||||
git:
|
||||
paging:
|
||||
colorArg: always
|
||||
useConfig: false
|
||||
merging:
|
||||
# only applicable to unix users
|
||||
manualCommit: false
|
||||
# extra args passed to `git merge`, e.g. --no-ff
|
||||
args: ''
|
||||
pull:
|
||||
mode: 'merge' # one of 'merge' | 'rebase' | 'ff-only'
|
||||
skipHookPrefix: WIP
|
||||
autoFetch: true
|
||||
branchLogCmd: 'git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --'
|
||||
allBranchesLogCmd: 'git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium'
|
||||
overrideGpg: false # prevents lazygit from spawning a separate process when using GPG
|
||||
disableForcePushing: false
|
||||
os:
|
||||
editCommand: '' # see 'Configuring File Editing' section
|
||||
openCommand: ''
|
||||
refresher:
|
||||
refreshInterval: 10 # file/submodule refresh interval in seconds
|
||||
fetchInterval: 60 # re-fetch interval in seconds
|
||||
update:
|
||||
method: prompt # can be: prompt | background | never
|
||||
days: 14 # how often an update is checked for
|
||||
reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined'
|
||||
confirmOnQuit: false
|
||||
# determines whether hitting 'esc' will quit the application when there is nothing to cancel/close
|
||||
quitOnTopLevelReturn: false
|
||||
disableStartupPopups: false
|
||||
notARepository: 'prompt' # one of: 'prompt' | 'create' | 'skip'
|
||||
keybinding:
|
||||
universal:
|
||||
quit: 'q'
|
||||
quit-alt1: '<c-c>' # alternative/alias of quit
|
||||
return: '<esc>' # return to previous menu, will quit if there's nowhere to return
|
||||
quitWithoutChangingDirectory: 'Q'
|
||||
togglePanel: '<tab>' # goto the next panel
|
||||
prevItem: '<up>' # go one line up
|
||||
nextItem: '<down>' # go one line down
|
||||
prevItem-alt: 'k' # go one line up
|
||||
nextItem-alt: 'j' # go one line down
|
||||
prevPage: ',' # go to next page in list
|
||||
nextPage: '.' # go to previous page in list
|
||||
gotoTop: '<' # go to top of list
|
||||
gotoBottom: '>' # go to bottom of list
|
||||
prevBlock: '<left>' # goto the previous block / panel
|
||||
nextBlock: '<right>' # goto the next block / panel
|
||||
prevBlock-alt: 'h' # goto the previous block / panel
|
||||
nextBlock-alt: 'l' # goto the next block / panel
|
||||
nextMatch: 'n'
|
||||
prevMatch: 'N'
|
||||
optionMenu: 'x' # show help menu
|
||||
optionMenu-alt1: '?' # show help menu
|
||||
select: '<space>'
|
||||
goInto: '<enter>'
|
||||
openRecentRepos: '<c-r>'
|
||||
confirm: '<enter>'
|
||||
confirm-alt1: 'y'
|
||||
remove: 'd'
|
||||
new: 'n'
|
||||
edit: 'e'
|
||||
openFile: 'o'
|
||||
scrollUpMain: '<pgup>' # main panel scroll up
|
||||
scrollDownMain: '<pgdown>' # main panel scroll down
|
||||
scrollUpMain-alt1: 'K' # main panel scroll up
|
||||
scrollDownMain-alt1: 'J' # main panel scroll down
|
||||
scrollUpMain-alt2: '<c-u>' # main panel scroll up
|
||||
scrollDownMain-alt2: '<c-d>' # main panel scroll down
|
||||
executeCustomCommand: ':'
|
||||
createRebaseOptionsMenu: 'm'
|
||||
pushFiles: 'P'
|
||||
pullFiles: 'p'
|
||||
refresh: 'R'
|
||||
createPatchOptionsMenu: '<c-p>'
|
||||
nextTab: ']'
|
||||
prevTab: '['
|
||||
nextScreenMode: '+'
|
||||
prevScreenMode: '_'
|
||||
undo: 'z'
|
||||
redo: '<c-z>'
|
||||
filteringMenu: '<c-s>'
|
||||
diffingMenu: 'W'
|
||||
diffingMenu-alt: '<c-e>' # deprecated
|
||||
copyToClipboard: '<c-o>'
|
||||
submitEditorText: '<enter>'
|
||||
appendNewline: '<tab>'
|
||||
status:
|
||||
checkForUpdate: 'u'
|
||||
recentRepos: '<enter>'
|
||||
files:
|
||||
commitChanges: 'c'
|
||||
commitChangesWithoutHook: 'w' # commit changes without pre-commit hook
|
||||
amendLastCommit: 'A'
|
||||
commitChangesWithEditor: 'C'
|
||||
ignoreFile: 'i'
|
||||
refreshFiles: 'r'
|
||||
stashAllChanges: 's'
|
||||
viewStashOptions: 'S'
|
||||
toggleStagedAll: 'a' # stage/unstage all
|
||||
viewResetOptions: 'D'
|
||||
fetch: 'f'
|
||||
toggleTreeView: '`'
|
||||
branches:
|
||||
createPullRequest: 'o'
|
||||
checkoutBranchByName: 'c'
|
||||
forceCheckoutBranch: 'F'
|
||||
rebaseBranch: 'r'
|
||||
mergeIntoCurrentBranch: 'M'
|
||||
viewGitFlowOptions: 'i'
|
||||
fastForward: 'f' # fast-forward this branch from its upstream
|
||||
pushTag: 'P'
|
||||
setUpstream: 'u' # set as upstream of checked-out branch
|
||||
fetchRemote: 'f'
|
||||
commits:
|
||||
squashDown: 's'
|
||||
renameCommit: 'r'
|
||||
renameCommitWithEditor: 'R'
|
||||
viewResetOptions: 'g'
|
||||
markCommitAsFixup: 'f'
|
||||
createFixupCommit: 'F' # create fixup commit for this commit
|
||||
squashAboveCommits: 'S'
|
||||
moveDownCommit: '<c-j>' # move commit down one
|
||||
moveUpCommit: '<c-k>' # move commit up one
|
||||
amendToCommit: 'A'
|
||||
pickCommit: 'p' # pick commit (when mid-rebase)
|
||||
revertCommit: 't'
|
||||
cherryPickCopy: 'c'
|
||||
cherryPickCopyRange: 'C'
|
||||
pasteCommits: 'v'
|
||||
tagCommit: 'T'
|
||||
checkoutCommit: '<space>'
|
||||
resetCherryPick: '<c-R>'
|
||||
copyCommitMessageToClipboard: '<c-y>'
|
||||
stash:
|
||||
popStash: 'g'
|
||||
commitFiles:
|
||||
checkoutCommitFile: 'c'
|
||||
main:
|
||||
toggleDragSelect: 'v'
|
||||
toggleDragSelect-alt: 'V'
|
||||
toggleSelectHunk: 'a'
|
||||
pickBothHunks: 'b'
|
||||
submodules:
|
||||
init: 'i'
|
||||
update: 'u'
|
||||
bulkMenu: 'b'
|
||||
```
|
||||
|
||||
## Platform Defaults
|
||||
@@ -186,31 +199,50 @@ Default path for the config file:
|
||||
### Windows
|
||||
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'cmd /c "start "" {{filename}}"'
|
||||
os:
|
||||
openCommand: 'cmd /c "start "" {{filename}}"'
|
||||
```
|
||||
|
||||
### Linux
|
||||
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'
|
||||
os:
|
||||
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'
|
||||
```
|
||||
|
||||
### OSX
|
||||
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'open {{filename}}'
|
||||
os:
|
||||
openCommand: 'open {{filename}}'
|
||||
```
|
||||
|
||||
### Configuring File Editing
|
||||
|
||||
Lazygit will edit a file with the first set editor in the following:
|
||||
|
||||
1. config.yaml
|
||||
|
||||
```yaml
|
||||
os:
|
||||
editCommand: 'vim' # as an example
|
||||
```
|
||||
|
||||
2. \$(git config core.editor)
|
||||
3. \$GIT_EDITOR
|
||||
4. \$VISUAL
|
||||
5. \$EDITOR
|
||||
6. \$(which vi)
|
||||
|
||||
Lazygit will log an error if none of these options are set.
|
||||
|
||||
### Recommended Config Values
|
||||
|
||||
for users of VSCode
|
||||
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'code -rg {{filename}}'
|
||||
os:
|
||||
openCommand: 'code -rg {{filename}}'
|
||||
```
|
||||
|
||||
## Color Attributes
|
||||
@@ -236,16 +268,16 @@ The available attributes are:
|
||||
If you have issues with a light terminal theme where you can't read / see the text add these settings
|
||||
|
||||
```yaml
|
||||
gui:
|
||||
theme:
|
||||
lightTheme: true
|
||||
activeBorderColor:
|
||||
- black
|
||||
- bold
|
||||
inactiveBorderColor:
|
||||
- black
|
||||
selectedLineBgColor:
|
||||
- default
|
||||
gui:
|
||||
theme:
|
||||
lightTheme: true
|
||||
activeBorderColor:
|
||||
- black
|
||||
- bold
|
||||
inactiveBorderColor:
|
||||
- black
|
||||
selectedLineBgColor:
|
||||
- default
|
||||
```
|
||||
|
||||
## Struggling to see selected line
|
||||
@@ -253,15 +285,16 @@ If you have issues with a light terminal theme where you can't read / see the te
|
||||
If you struggle to see the selected line I recommend using the reverse attribute on selected lines like so:
|
||||
|
||||
```yaml
|
||||
gui:
|
||||
theme:
|
||||
selectedLineBgColor:
|
||||
- reverse
|
||||
selectedRangeBgColor:
|
||||
- reverse
|
||||
gui:
|
||||
theme:
|
||||
selectedLineBgColor:
|
||||
- reverse
|
||||
selectedRangeBgColor:
|
||||
- reverse
|
||||
```
|
||||
|
||||
The following has also worked for a couple of people:
|
||||
|
||||
```yaml
|
||||
gui:
|
||||
theme:
|
||||
@@ -290,33 +323,33 @@ For all possible keybinding options, check [Custom_Keybindings.md](https://githu
|
||||
### Example Keybindings For Colemak Users
|
||||
|
||||
```yaml
|
||||
keybinding:
|
||||
universal:
|
||||
prevItem-alt: 'u'
|
||||
nextItem-alt: 'e'
|
||||
prevBlock-alt: 'n'
|
||||
nextBlock-alt: 'i'
|
||||
nextMatch: '='
|
||||
prevMatch: '-'
|
||||
new: 'k'
|
||||
edit: 'o'
|
||||
openFile: 'O'
|
||||
scrollUpMain-alt1: 'U'
|
||||
scrollDownMain-alt1: 'E'
|
||||
scrollUpMain-alt2: '<c-u>'
|
||||
scrollDownMain-alt2: '<c-e>'
|
||||
undo: 'l'
|
||||
redo: '<c-r>'
|
||||
diffingMenu: 'M'
|
||||
filteringMenu: '<c-f>'
|
||||
files:
|
||||
ignoreFile: 'I'
|
||||
commits:
|
||||
moveDownCommit: '<c-e>'
|
||||
moveUpCommit: '<c-u>'
|
||||
branches:
|
||||
viewGitFlowOptions: 'I'
|
||||
setUpstream: 'U'
|
||||
keybinding:
|
||||
universal:
|
||||
prevItem-alt: 'u'
|
||||
nextItem-alt: 'e'
|
||||
prevBlock-alt: 'n'
|
||||
nextBlock-alt: 'i'
|
||||
nextMatch: '='
|
||||
prevMatch: '-'
|
||||
new: 'k'
|
||||
edit: 'o'
|
||||
openFile: 'O'
|
||||
scrollUpMain-alt1: 'U'
|
||||
scrollDownMain-alt1: 'E'
|
||||
scrollUpMain-alt2: '<c-u>'
|
||||
scrollDownMain-alt2: '<c-e>'
|
||||
undo: 'l'
|
||||
redo: '<c-r>'
|
||||
diffingMenu: 'M'
|
||||
filteringMenu: '<c-f>'
|
||||
files:
|
||||
ignoreFile: 'I'
|
||||
commits:
|
||||
moveDownCommit: '<c-e>'
|
||||
moveUpCommit: '<c-u>'
|
||||
branches:
|
||||
viewGitFlowOptions: 'I'
|
||||
setUpstream: 'U'
|
||||
```
|
||||
|
||||
## Custom pull request URLs
|
||||
@@ -327,7 +360,7 @@ the pull request. You can do so on your `config.yml` file using the following sy
|
||||
|
||||
```yaml
|
||||
services:
|
||||
"<gitDomain>": "<provider>:<webDomain>"
|
||||
'<gitDomain>': '<provider>:<webDomain>'
|
||||
```
|
||||
|
||||
Where:
|
||||
@@ -337,19 +370,21 @@ Where:
|
||||
- `webDomain` is the URL where your git service exposes a web interface and APIs, e.g. `gitservice.work.com`
|
||||
|
||||
## Predefined commit message prefix
|
||||
|
||||
In situations where certain naming pattern is used for branches and commits, pattern can be used to populate
|
||||
commit message with prefix that is parsed from the branch name.
|
||||
|
||||
Example:
|
||||
* Branch name: feature/AB-123
|
||||
* Commit message: [AB-123] Adding feature
|
||||
|
||||
- Branch name: feature/AB-123
|
||||
- Commit message: [AB-123] Adding feature
|
||||
|
||||
```yaml
|
||||
git:
|
||||
commitPrefixes:
|
||||
my_project: # This is repository folder name
|
||||
pattern: "^\\w+\\/(\\w+-\\w+).*"
|
||||
replace: "[$1] "
|
||||
git:
|
||||
commitPrefixes:
|
||||
my_project: # This is repository folder name
|
||||
pattern: "^\\w+\\/(\\w+-\\w+).*"
|
||||
replace: '[$1] '
|
||||
```
|
||||
|
||||
## Custom git log command
|
||||
|
||||
6
go.mod
6
go.mod
@@ -20,7 +20,7 @@ require (
|
||||
github.com/imdario/mergo v0.3.11
|
||||
github.com/integrii/flaggy v1.4.0
|
||||
github.com/jesseduffield/go-git/v5 v5.1.2-0.20201006095850-341962be15a4
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210406065942-1b0c68414064
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210417110745-37f79434200d
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe // indirect
|
||||
github.com/jesseduffield/yaml v2.1.0+incompatible
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
|
||||
@@ -39,8 +39,8 @@ require (
|
||||
github.com/stretchr/testify v1.4.0
|
||||
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-20210403161142-5e06dd20ab57 // indirect
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 // indirect
|
||||
golang.org/x/sys v0.0.0-20210415045647-66c3f260301c // indirect
|
||||
golang.org/x/term v0.0.0-20210406210042-72f3dc4e9b72 // indirect
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
)
|
||||
|
||||
|
||||
18
go.sum
18
go.sum
@@ -106,6 +106,20 @@ github.com/jesseduffield/gocui v0.3.1-0.20210406065811-95ef6e13779b h1:3+4+muhhi
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210406065811-95ef6e13779b/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210406065942-1b0c68414064 h1:Oe+QJuUIOd2TU+A3BW5sT1eXqceoBcOOfyoHlGf7F8Y=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210406065942-1b0c68414064/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210409121040-210802112d8a h1:ocrSuZxQIgWWt27b+rjiyIIPz6fzfFeoL5Q4cpa2cAo=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210409121040-210802112d8a/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210410011117-a2bb4baca390 h1:Es72JiUjt01TtvqCugdvOR91baB3DhuWF1DNuxA0frA=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210410011117-a2bb4baca390/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210412111008-6ef019af3724 h1:U70Do3/OSw5n/oLJGPWsQHnos2p0yq8yAeD2muioJhQ=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210412111008-6ef019af3724/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210412113212-ee65bd542c08 h1:d003y2GByfR3PqN/JvxNuqyo8vx4m0epwY2hW7sNU80=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210412113212-ee65bd542c08/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210412130453-de7bb5079f9f h1:JPpHlvSrKNxro+K9rM3nEHCdZ16qD0hnEedHPF07OtA=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210412130453-de7bb5079f9f/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210417105214-bdf37de5c917 h1:H4THGOdAJf61wByuq8EHF/NAgtqrTxpSIPsrCXU9HAY=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210417105214-bdf37de5c917/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210417110745-37f79434200d h1:2BPcc19W0j576hvhxtKma4jcD/+qAYvw1ln2HcIEZGU=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20210417110745-37f79434200d/go.mod h1:QWq79xplEoyhQO+dgpk3sojjTVRKjQklyTlzm5nC5Kg=
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe h1:qsVhCf2RFyyKIUe/+gJblbCpXMUki9rZrHuEctg6M/E=
|
||||
github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe/go.mod h1:anMibpZtqNxjDbxrcDEAwSdaJ37vyUeM1f/M4uekib4=
|
||||
github.com/jesseduffield/yaml v2.1.0+incompatible h1:HWQJ1gIv2zHKbDYNp0Jwjlj24K8aqpFHnMCynY1EpmE=
|
||||
@@ -226,10 +240,14 @@ golang.org/x/sys v0.0.0-20210402192133-700132347e07 h1:4k6HsQjxj6hVMsI2Vf0yKlzt5
|
||||
golang.org/x/sys v0.0.0-20210402192133-700132347e07/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210415045647-66c3f260301c h1:6L+uOeS3OQt/f4eFHXZcTxeZrGCuz+CLElgEBjbcTA4=
|
||||
golang.org/x/sys v0.0.0-20210415045647-66c3f260301c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 h1:EC6+IGYTjPpRfv9a2b/6Puw0W+hLtAhkV1tPsXhutqs=
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210406210042-72f3dc4e9b72 h1:VqE9gduFZ4dbR7XoL77lHFp0/DyDUBKSXK7CMFkVcV0=
|
||||
golang.org/x/term v0.0.0-20210406210042-72f3dc4e9b72/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
|
||||
3
main.go
3
main.go
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/integrii/flaggy"
|
||||
"github.com/jesseduffield/lazygit/pkg/app"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/constants"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
yaml "github.com/jesseduffield/yaml"
|
||||
)
|
||||
@@ -134,6 +135,6 @@ func main() {
|
||||
stackTrace := newErr.ErrorStack()
|
||||
app.Log.Error(stackTrace)
|
||||
|
||||
log.Fatal(fmt.Sprintf("%s\n\n%s", app.Tr.ErrorOccurred, stackTrace))
|
||||
log.Fatal(fmt.Sprintf("%s: %s\n\n%s", app.Tr.ErrorOccurred, constants.Links.Issues, stackTrace))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,9 +59,9 @@ type CheckoutOptions struct {
|
||||
func (c *GitCommand) Checkout(branch string, options CheckoutOptions) error {
|
||||
forceArg := ""
|
||||
if options.Force {
|
||||
forceArg = "--force "
|
||||
forceArg = " --force"
|
||||
}
|
||||
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git checkout %s %s", forceArg, branch), oscommands.RunCommandOptions{EnvVars: options.EnvVars})
|
||||
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git checkout%s %s", forceArg, branch), oscommands.RunCommandOptions{EnvVars: options.EnvVars})
|
||||
}
|
||||
|
||||
// GetBranchGraph gets the color-formatted graph of the log for the given branch
|
||||
@@ -152,6 +152,10 @@ func (c *GitCommand) ResetSoft(ref string) error {
|
||||
return c.RunCommand("git reset --soft " + ref)
|
||||
}
|
||||
|
||||
func (c *GitCommand) ResetMixed(ref string) error {
|
||||
return c.RunCommand("git reset --mixed " + ref)
|
||||
}
|
||||
|
||||
func (c *GitCommand) RenameBranch(oldName string, newName string) error {
|
||||
return c.RunCommand("git branch --move %s %s", oldName, newName)
|
||||
}
|
||||
|
||||
338
pkg/commands/branches_test.go
Normal file
338
pkg/commands/branches_test.go
Normal file
@@ -0,0 +1,338 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/test"
|
||||
"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)
|
||||
}
|
||||
|
||||
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)
|
||||
},
|
||||
},
|
||||
{
|
||||
"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)
|
||||
},
|
||||
},
|
||||
{
|
||||
"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)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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}"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.NewBranch("test", "master"))
|
||||
}
|
||||
|
||||
// TestGitCommandDeleteBranch is a function.
|
||||
func TestGitCommandDeleteBranch(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
branch string
|
||||
force bool
|
||||
command func(string, ...string) *exec.Cmd
|
||||
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")
|
||||
},
|
||||
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")
|
||||
},
|
||||
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
|
||||
s.test(gitCmd.DeleteBranch(s.branch, s.force))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.Merge("test", MergeOpts{}))
|
||||
}
|
||||
|
||||
// TestGitCommandCheckout is a function.
|
||||
func TestGitCommandCheckout(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
force bool
|
||||
}
|
||||
|
||||
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")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"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")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.Checkout("test", CheckoutOptions{Force: s.force}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
_, 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)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// TestGitCommandCurrentBranchName is a function.
|
||||
func TestGitCommandCurrentBranchName(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
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")
|
||||
},
|
||||
func(name string, displayname string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "master", name)
|
||||
assert.EqualValues(t, "master", displayname)
|
||||
},
|
||||
},
|
||||
{
|
||||
"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
|
||||
},
|
||||
func(name string, displayname string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "master", name)
|
||||
assert.EqualValues(t, "master", displayname)
|
||||
},
|
||||
},
|
||||
{
|
||||
"handles a detached head",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
|
||||
switch args[0] {
|
||||
case "symbolic-ref":
|
||||
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
|
||||
return secureexec.Command("test")
|
||||
case "branch":
|
||||
assert.EqualValues(t, []string{"branch", "--contains"}, args)
|
||||
return secureexec.Command("echo", "* (HEAD detached at 123abcd)")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
func(name string, displayname string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, "123abcd", name)
|
||||
assert.EqualValues(t, "(HEAD detached at 123abcd)", displayname)
|
||||
},
|
||||
},
|
||||
{
|
||||
"bubbles up error if there is one",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.Equal(t, "git", cmd)
|
||||
return secureexec.Command("test")
|
||||
},
|
||||
func(name string, displayname string, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.EqualValues(t, "", name)
|
||||
assert.EqualValues(t, "", displayname)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.CurrentBranchName())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandResetHard is a function.
|
||||
func TestGitCommandResetHard(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
ref string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
"HEAD",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: `git reset --hard HEAD`,
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
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
|
||||
s.test(gitCmd.ResetHard(s.ref))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
@@ -19,20 +18,19 @@ func (c *GitCommand) ResetToCommit(sha string, strength string, options oscomman
|
||||
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git reset --%s %s", strength, sha), options)
|
||||
}
|
||||
|
||||
// Commit commits to git
|
||||
func (c *GitCommand) Commit(message string, flags string) (*exec.Cmd, error) {
|
||||
func (c *GitCommand) CommitCmdStr(message string, flags string) string {
|
||||
splitMessage := strings.Split(message, "\n")
|
||||
lineArgs := ""
|
||||
for _, line := range splitMessage {
|
||||
lineArgs += fmt.Sprintf(" -m %s", c.OSCommand.Quote(line))
|
||||
}
|
||||
|
||||
command := fmt.Sprintf("git commit %s%s", flags, lineArgs)
|
||||
if c.usingGpg() {
|
||||
return c.OSCommand.ShellCommandFromString(command), nil
|
||||
flagsStr := ""
|
||||
if flags != "" {
|
||||
flagsStr = fmt.Sprintf(" %s", flags)
|
||||
}
|
||||
|
||||
return nil, c.OSCommand.RunCommand(command)
|
||||
return fmt.Sprintf("git commit%s%s", flagsStr, lineArgs)
|
||||
}
|
||||
|
||||
// Get the subject of the HEAD commit
|
||||
@@ -50,18 +48,12 @@ func (c *GitCommand) GetCommitMessage(commitSha string) (string, error) {
|
||||
}
|
||||
|
||||
// AmendHead amends HEAD with whatever is staged in your working tree
|
||||
func (c *GitCommand) AmendHead() (*exec.Cmd, error) {
|
||||
command := "git commit --amend --no-edit --allow-empty"
|
||||
if c.usingGpg() {
|
||||
return c.OSCommand.ShellCommandFromString(command), nil
|
||||
}
|
||||
|
||||
return nil, c.OSCommand.RunCommand(command)
|
||||
func (c *GitCommand) AmendHead() error {
|
||||
return c.OSCommand.RunCommand(c.AmendHeadCmdStr())
|
||||
}
|
||||
|
||||
// PrepareCommitAmendSubProcess prepares a subprocess for `git commit --amend --allow-empty`
|
||||
func (c *GitCommand) PrepareCommitAmendSubProcess() *exec.Cmd {
|
||||
return c.OSCommand.PrepareSubProcess("git", "commit", "--amend", "--allow-empty")
|
||||
func (c *GitCommand) AmendHeadCmdStr() string {
|
||||
return "git commit --amend --no-edit --allow-empty"
|
||||
}
|
||||
|
||||
func (c *GitCommand) ShowCmdStr(sha string, filterPath string) string {
|
||||
|
||||
111
pkg/commands/commits_test.go
Normal file
111
pkg/commands/commits_test.go
Normal file
@@ -0,0 +1,111 @@
|
||||
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")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.RenameCommit("test"))
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.ResetToCommit("78976bc", "hard", oscommands.RunCommandOptions{}))
|
||||
}
|
||||
|
||||
// TestGitCommandCommitStr is a function.
|
||||
func TestGitCommandCommitStr(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
message string
|
||||
flags string
|
||||
expected string
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
testName: "Commit",
|
||||
message: "test",
|
||||
flags: "",
|
||||
expected: "git commit -m \"test\"",
|
||||
},
|
||||
{
|
||||
testName: "Commit with --no-verify flag",
|
||||
message: "test",
|
||||
flags: "--no-verify",
|
||||
expected: "git commit --no-verify -m \"test\"",
|
||||
},
|
||||
{
|
||||
testName: "Commit with multiline message",
|
||||
message: "line1\nline2",
|
||||
flags: "",
|
||||
expected: "git commit -m \"line1\" -m \"line2\"",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
cmdStr := gitCmd.CommitCmdStr(s.message, s.flags)
|
||||
assert.Equal(t, s.expected, cmdStr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandCreateFixupCommit is a function.
|
||||
func TestGitCommandCreateFixupCommit(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
sha string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
"12345",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: `git commit --fixup=12345`,
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
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
|
||||
s.test(gitCmd.CreateFixupCommit(s.sha))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -46,3 +46,17 @@ func (c *GitCommand) GetConfigValue(key string) string {
|
||||
output, _ := c.getGitConfigValue(key)
|
||||
return output
|
||||
}
|
||||
|
||||
// UsingGpg tells us whether the user has gpg enabled so that we can know
|
||||
// whether we need to run a subprocess to allow them to enter their password
|
||||
func (c *GitCommand) UsingGpg() bool {
|
||||
overrideGpg := c.Config.GetUserConfig().Git.OverrideGpg
|
||||
if overrideGpg {
|
||||
return false
|
||||
}
|
||||
|
||||
gpgsign := c.GetConfigValue("commit.gpgsign")
|
||||
value := strings.ToLower(gpgsign)
|
||||
|
||||
return value == "true" || value == "1" || value == "yes" || value == "on"
|
||||
}
|
||||
|
||||
70
pkg/commands/config_test.go
Normal file
70
pkg/commands/config_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandUsingGpg is a function.
|
||||
func TestGitCommandUsingGpg(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
getGitConfigValue func(string) (string, error)
|
||||
test func(bool)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Option global and local config commit.gpgsign is not set",
|
||||
func(string) (string, error) { return "", nil },
|
||||
func(gpgEnabled bool) {
|
||||
assert.False(t, gpgEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Option commit.gpgsign is true",
|
||||
func(string) (string, error) {
|
||||
return "True", nil
|
||||
},
|
||||
func(gpgEnabled bool) {
|
||||
assert.True(t, gpgEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Option commit.gpgsign is on",
|
||||
func(string) (string, error) {
|
||||
return "ON", nil
|
||||
},
|
||||
func(gpgEnabled bool) {
|
||||
assert.True(t, gpgEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Option commit.gpgsign is yes",
|
||||
func(string) (string, error) {
|
||||
return "YeS", nil
|
||||
},
|
||||
func(gpgEnabled bool) {
|
||||
assert.True(t, gpgEnabled)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Option commit.gpgsign is 1",
|
||||
func(string) (string, error) {
|
||||
return "1", nil
|
||||
},
|
||||
func(gpgEnabled bool) {
|
||||
assert.True(t, gpgEnabled)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.getGitConfigValue = s.getGitConfigValue
|
||||
s.test(gitCmd.UsingGpg())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,5 @@ func NewDummyGitCommandWithOSCommand(osCommand *oscommands.OSCommand) *GitComman
|
||||
Tr: i18n.NewTranslationSet(utils.NewDummyLog()),
|
||||
Config: config.NewDummyAppConfig(),
|
||||
getGitConfigValue: func(string) (string, error) { return "", nil },
|
||||
removeFile: func(string) error { return nil },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package commands
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
@@ -11,7 +10,6 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/mgutz/str"
|
||||
)
|
||||
|
||||
// CatFile obtains the content of a file
|
||||
@@ -19,6 +17,14 @@ func (c *GitCommand) CatFile(fileName string) (string, error) {
|
||||
return c.OSCommand.CatFile(fileName)
|
||||
}
|
||||
|
||||
func (c *GitCommand) OpenMergeToolCmd() string {
|
||||
return "git mergetool"
|
||||
}
|
||||
|
||||
func (c *GitCommand) OpenMergeTool() error {
|
||||
return c.OSCommand.RunCommand("git mergetool")
|
||||
}
|
||||
|
||||
// StageFile stages a file
|
||||
func (c *GitCommand) StageFile(fileName string) error {
|
||||
return c.RunCommand("git add -- %s", c.OSCommand.Quote(fileName))
|
||||
@@ -133,7 +139,7 @@ func (c *GitCommand) DiscardAllFileChanges(file *models.File) error {
|
||||
}
|
||||
|
||||
if file.Added {
|
||||
return c.removeFile(file.Name)
|
||||
return c.OSCommand.RemoveFile(file.Name)
|
||||
}
|
||||
return c.DiscardUnstagedFileChanges(file)
|
||||
}
|
||||
@@ -267,10 +273,7 @@ func (c *GitCommand) DiscardOldFileChanges(commits []*models.Commit, commitIndex
|
||||
}
|
||||
|
||||
// amend the commit
|
||||
cmd, err := c.AmendHead()
|
||||
if cmd != nil {
|
||||
return errors.New("received unexpected pointer to cmd")
|
||||
}
|
||||
err := c.AmendHead()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -314,10 +317,12 @@ func (c *GitCommand) ResetAndClean() error {
|
||||
return c.RemoveUntrackedFiles()
|
||||
}
|
||||
|
||||
// EditFile opens a file in a subprocess using whatever editor is available,
|
||||
// falling back to core.editor, GIT_EDITOR, VISUAL, EDITOR, then vi
|
||||
func (c *GitCommand) EditFile(filename string) (*exec.Cmd, error) {
|
||||
editor := c.GetConfigValue("core.editor")
|
||||
func (c *GitCommand) EditFileCmdStr(filename string) (string, error) {
|
||||
editor := c.Config.GetUserConfig().OS.EditCommand
|
||||
|
||||
if editor == "" {
|
||||
editor = c.GetConfigValue("core.editor")
|
||||
}
|
||||
|
||||
if editor == "" {
|
||||
editor = c.OSCommand.Getenv("GIT_EDITOR")
|
||||
@@ -334,10 +339,8 @@ func (c *GitCommand) EditFile(filename string) (*exec.Cmd, error) {
|
||||
}
|
||||
}
|
||||
if editor == "" {
|
||||
return nil, errors.New("No editor defined in $GIT_EDITOR, $VISUAL, $EDITOR, or git config")
|
||||
return "", errors.New("No editor defined in config file, $GIT_EDITOR, $VISUAL, $EDITOR, or git config")
|
||||
}
|
||||
|
||||
splitCmd := str.ToArgv(fmt.Sprintf("%s %s", editor, c.OSCommand.Quote(filename)))
|
||||
|
||||
return c.OSCommand.PrepareSubProcess(splitCmd[0], splitCmd[1:]...), nil
|
||||
return fmt.Sprintf("%s %s", editor, c.OSCommand.Quote(filename)), nil
|
||||
}
|
||||
|
||||
872
pkg/commands/files_test.go
Normal file
872
pkg/commands/files_test.go
Normal file
@@ -0,0 +1,872 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandCatFile tests emitting a file using commands, where commands vary by OS.
|
||||
func TestGitCommandCatFile(t *testing.T) {
|
||||
var osCmd string
|
||||
switch os := runtime.GOOS; os {
|
||||
case "windows":
|
||||
osCmd = "type"
|
||||
default:
|
||||
osCmd = "cat"
|
||||
}
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, osCmd, cmd)
|
||||
assert.EqualValues(t, []string{"test.txt"}, args)
|
||||
|
||||
return secureexec.Command("echo", "-n", "test")
|
||||
}
|
||||
|
||||
o, err := gitCmd.CatFile("test.txt")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test", o)
|
||||
}
|
||||
|
||||
// TestGitCommandStageFile is a function.
|
||||
func TestGitCommandStageFile(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"add", "--", "test.txt"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.StageFile("test.txt"))
|
||||
}
|
||||
|
||||
// TestGitCommandUnstageFile is a function.
|
||||
func TestGitCommandUnstageFile(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
reset bool
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Remove an untracked file from staging",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"rm", "--cached", "--force", "--", "test.txt"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Remove a tracked file from staging",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"reset", "HEAD", "--", "test.txt"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.UnStageFile([]string{"test.txt"}, s.reset))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandDiscardAllFileChanges is a function.
|
||||
// these tests don't cover everything, in part because we already have an integration
|
||||
// test which does cover everything. I don't want to unnecessarily assert on the 'how'
|
||||
// when the 'what' is what matters
|
||||
func TestGitCommandDiscardAllFileChanges(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func() (func(string, ...string) *exec.Cmd, *[][]string)
|
||||
test func(*[][]string, error)
|
||||
file *models.File
|
||||
removeFile func(string) error
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"An error occurred when resetting",
|
||||
func() (func(string, ...string) *exec.Cmd, *[][]string) {
|
||||
cmdsCalled := [][]string{}
|
||||
return func(cmd string, args ...string) *exec.Cmd {
|
||||
cmdsCalled = append(cmdsCalled, args)
|
||||
|
||||
return secureexec.Command("test")
|
||||
}, &cmdsCalled
|
||||
},
|
||||
func(cmdsCalled *[][]string, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.Len(t, *cmdsCalled, 1)
|
||||
assert.EqualValues(t, *cmdsCalled, [][]string{
|
||||
{"reset", "--", "test"},
|
||||
})
|
||||
},
|
||||
&models.File{
|
||||
Name: "test",
|
||||
HasStagedChanges: true,
|
||||
},
|
||||
func(string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
"An error occurred when removing file",
|
||||
func() (func(string, ...string) *exec.Cmd, *[][]string) {
|
||||
cmdsCalled := [][]string{}
|
||||
return func(cmd string, args ...string) *exec.Cmd {
|
||||
cmdsCalled = append(cmdsCalled, args)
|
||||
|
||||
return secureexec.Command("test")
|
||||
}, &cmdsCalled
|
||||
},
|
||||
func(cmdsCalled *[][]string, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.EqualError(t, err, "an error occurred when removing file")
|
||||
assert.Len(t, *cmdsCalled, 0)
|
||||
},
|
||||
&models.File{
|
||||
Name: "test",
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
},
|
||||
func(string) error {
|
||||
return fmt.Errorf("an error occurred when removing file")
|
||||
},
|
||||
},
|
||||
{
|
||||
"An error occurred with checkout",
|
||||
func() (func(string, ...string) *exec.Cmd, *[][]string) {
|
||||
cmdsCalled := [][]string{}
|
||||
return func(cmd string, args ...string) *exec.Cmd {
|
||||
cmdsCalled = append(cmdsCalled, args)
|
||||
|
||||
return secureexec.Command("test")
|
||||
}, &cmdsCalled
|
||||
},
|
||||
func(cmdsCalled *[][]string, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.Len(t, *cmdsCalled, 1)
|
||||
assert.EqualValues(t, *cmdsCalled, [][]string{
|
||||
{"checkout", "--", "test"},
|
||||
})
|
||||
},
|
||||
&models.File{
|
||||
Name: "test",
|
||||
Tracked: true,
|
||||
HasStagedChanges: false,
|
||||
},
|
||||
func(string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
"Checkout only",
|
||||
func() (func(string, ...string) *exec.Cmd, *[][]string) {
|
||||
cmdsCalled := [][]string{}
|
||||
return func(cmd string, args ...string) *exec.Cmd {
|
||||
cmdsCalled = append(cmdsCalled, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}, &cmdsCalled
|
||||
},
|
||||
func(cmdsCalled *[][]string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, *cmdsCalled, 1)
|
||||
assert.EqualValues(t, *cmdsCalled, [][]string{
|
||||
{"checkout", "--", "test"},
|
||||
})
|
||||
},
|
||||
&models.File{
|
||||
Name: "test",
|
||||
Tracked: true,
|
||||
HasStagedChanges: false,
|
||||
},
|
||||
func(string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
"Reset and checkout staged changes",
|
||||
func() (func(string, ...string) *exec.Cmd, *[][]string) {
|
||||
cmdsCalled := [][]string{}
|
||||
return func(cmd string, args ...string) *exec.Cmd {
|
||||
cmdsCalled = append(cmdsCalled, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}, &cmdsCalled
|
||||
},
|
||||
func(cmdsCalled *[][]string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, *cmdsCalled, 2)
|
||||
assert.EqualValues(t, *cmdsCalled, [][]string{
|
||||
{"reset", "--", "test"},
|
||||
{"checkout", "--", "test"},
|
||||
})
|
||||
},
|
||||
&models.File{
|
||||
Name: "test",
|
||||
Tracked: true,
|
||||
HasStagedChanges: true,
|
||||
},
|
||||
func(string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
"Reset and checkout merge conflicts",
|
||||
func() (func(string, ...string) *exec.Cmd, *[][]string) {
|
||||
cmdsCalled := [][]string{}
|
||||
return func(cmd string, args ...string) *exec.Cmd {
|
||||
cmdsCalled = append(cmdsCalled, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}, &cmdsCalled
|
||||
},
|
||||
func(cmdsCalled *[][]string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, *cmdsCalled, 2)
|
||||
assert.EqualValues(t, *cmdsCalled, [][]string{
|
||||
{"reset", "--", "test"},
|
||||
{"checkout", "--", "test"},
|
||||
})
|
||||
},
|
||||
&models.File{
|
||||
Name: "test",
|
||||
Tracked: true,
|
||||
HasMergeConflicts: true,
|
||||
},
|
||||
func(string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
"Reset and remove",
|
||||
func() (func(string, ...string) *exec.Cmd, *[][]string) {
|
||||
cmdsCalled := [][]string{}
|
||||
return func(cmd string, args ...string) *exec.Cmd {
|
||||
cmdsCalled = append(cmdsCalled, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}, &cmdsCalled
|
||||
},
|
||||
func(cmdsCalled *[][]string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, *cmdsCalled, 1)
|
||||
assert.EqualValues(t, *cmdsCalled, [][]string{
|
||||
{"reset", "--", "test"},
|
||||
})
|
||||
},
|
||||
&models.File{
|
||||
Name: "test",
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
HasStagedChanges: true,
|
||||
},
|
||||
func(filename string) error {
|
||||
assert.Equal(t, "test", filename)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
"Remove only",
|
||||
func() (func(string, ...string) *exec.Cmd, *[][]string) {
|
||||
cmdsCalled := [][]string{}
|
||||
return func(cmd string, args ...string) *exec.Cmd {
|
||||
cmdsCalled = append(cmdsCalled, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
}, &cmdsCalled
|
||||
},
|
||||
func(cmdsCalled *[][]string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, *cmdsCalled, 0)
|
||||
},
|
||||
&models.File{
|
||||
Name: "test",
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
HasStagedChanges: false,
|
||||
},
|
||||
func(filename string) error {
|
||||
assert.Equal(t, "test", filename)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
var cmdsCalled *[][]string
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command, cmdsCalled = s.command()
|
||||
gitCmd.OSCommand.SetRemoveFile(s.removeFile)
|
||||
s.test(cmdsCalled, gitCmd.DiscardAllFileChanges(s.file))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandDiff is a function.
|
||||
func TestGitCommandDiff(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
file *models.File
|
||||
plain bool
|
||||
cached bool
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Default case",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=always", "--", "test.txt"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
&models.File{
|
||||
Name: "test.txt",
|
||||
HasStagedChanges: false,
|
||||
Tracked: true,
|
||||
},
|
||||
false,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"cached",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=always", "--cached", "--", "test.txt"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
&models.File{
|
||||
Name: "test.txt",
|
||||
HasStagedChanges: false,
|
||||
Tracked: true,
|
||||
},
|
||||
false,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"plain",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=never", "--", "test.txt"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
&models.File{
|
||||
Name: "test.txt",
|
||||
HasStagedChanges: false,
|
||||
Tracked: true,
|
||||
},
|
||||
true,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"File not tracked and file has no staged changes",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"diff", "--submodule", "--no-ext-diff", "--color=always", "--no-index", "--", "/dev/null", "test.txt"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
&models.File{
|
||||
Name: "test.txt",
|
||||
HasStagedChanges: false,
|
||||
Tracked: false,
|
||||
},
|
||||
false,
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
gitCmd.WorktreeFileDiff(s.file, s.plain, s.cached)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandCheckoutFile is a function.
|
||||
func TestGitCommandCheckoutFile(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
commitSha string
|
||||
fileName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"typical case",
|
||||
"11af912",
|
||||
"test999.txt",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git checkout 11af912 test999.txt",
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"returns error if there is one",
|
||||
"11af912",
|
||||
"test999.txt",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git checkout 11af912 test999.txt",
|
||||
Replace: "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
|
||||
s.test(gitCmd.CheckoutFile(s.commitSha, s.fileName))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitCommandApplyPatch(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.Equal(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"apply", "--cached"}, args[0:2])
|
||||
filename := args[2]
|
||||
content, err := ioutil.ReadFile(filename)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "test", string(content))
|
||||
|
||||
return secureexec.Command("echo", "done")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"command returns error",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.Equal(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"apply", "--cached"}, args[0:2])
|
||||
filename := args[2]
|
||||
// TODO: Ideally we want to mock out OSCommand here so that we're not
|
||||
// double handling testing it's CreateTempFile functionality,
|
||||
// but it is going to take a bit of work to make a proper mock for it
|
||||
// so I'm leaving it for another PR
|
||||
content, err := ioutil.ReadFile(filename)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "test", string(content))
|
||||
|
||||
return secureexec.Command("test")
|
||||
},
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.ApplyPatch("test", "cached"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandDiscardOldFileChanges is a function.
|
||||
func TestGitCommandDiscardOldFileChanges(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
getGitConfigValue func(string) (string, error)
|
||||
commits []*models.Commit
|
||||
commitIndex int
|
||||
fileName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"returns error when index outside of range of commits",
|
||||
func(string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
[]*models.Commit{},
|
||||
0,
|
||||
"test999.txt",
|
||||
nil,
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"returns error when using gpg",
|
||||
func(string) (string, error) {
|
||||
return "true", nil
|
||||
},
|
||||
[]*models.Commit{{Name: "commit", Sha: "123456"}},
|
||||
0,
|
||||
"test999.txt",
|
||||
nil,
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"checks out file if it already existed",
|
||||
func(string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
[]*models.Commit{
|
||||
{Name: "commit", Sha: "123456"},
|
||||
{Name: "commit2", Sha: "abcdef"},
|
||||
},
|
||||
0,
|
||||
"test999.txt",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: "git rebase --interactive --autostash --keep-empty abcdef",
|
||||
Replace: "echo",
|
||||
},
|
||||
{
|
||||
Expect: "git cat-file -e HEAD^:test999.txt",
|
||||
Replace: "echo",
|
||||
},
|
||||
{
|
||||
Expect: "git checkout HEAD^ test999.txt",
|
||||
Replace: "echo",
|
||||
},
|
||||
{
|
||||
Expect: "git commit --amend --no-edit --allow-empty",
|
||||
Replace: "echo",
|
||||
},
|
||||
{
|
||||
Expect: "git rebase --continue",
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
// test for when the file was created within the commit requires a refactor to support proper mocks
|
||||
// currently we'd need to mock out the os.Remove function and that's gonna introduce tech debt
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
gitCmd.getGitConfigValue = s.getGitConfigValue
|
||||
s.test(gitCmd.DiscardOldFileChanges(s.commits, s.commitIndex, s.fileName))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandDiscardUnstagedFileChanges is a function.
|
||||
func TestGitCommandDiscardUnstagedFileChanges(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
file *models.File
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
&models.File{Name: "test.txt"},
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: `git checkout -- "test.txt"`,
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
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
|
||||
s.test(gitCmd.DiscardUnstagedFileChanges(s.file))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandDiscardAnyUnstagedFileChanges is a function.
|
||||
func TestGitCommandDiscardAnyUnstagedFileChanges(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: `git checkout -- .`,
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
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
|
||||
s.test(gitCmd.DiscardAnyUnstagedFileChanges())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGitCommandRemoveUntrackedFiles is a function.
|
||||
func TestGitCommandRemoveUntrackedFiles(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"valid case",
|
||||
test.CreateMockCommand(t, []*test.CommandSwapper{
|
||||
{
|
||||
Expect: `git clean -fd`,
|
||||
Replace: "echo",
|
||||
},
|
||||
}),
|
||||
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
|
||||
s.test(gitCmd.RemoveUntrackedFiles())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestEditFileCmdStr is a function.
|
||||
func TestEditFileCmdStr(t *testing.T) {
|
||||
type scenario struct {
|
||||
filename string
|
||||
configEditCommand string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
getenv func(string) string
|
||||
getGitConfigValue func(string) (string, error)
|
||||
test func(string, error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"test",
|
||||
"",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
return secureexec.Command("exit", "1")
|
||||
},
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmdStr string, err error) {
|
||||
assert.EqualError(t, err, "No editor defined in config file, $GIT_EDITOR, $VISUAL, $EDITOR, or git config")
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
"nano",
|
||||
func(name string, args ...string) *exec.Cmd {
|
||||
assert.Equal(t, "which", name)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "nano \"test\"", cmdStr)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
"",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "which", name)
|
||||
return secureexec.Command("exit", "1")
|
||||
},
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "nano", nil
|
||||
},
|
||||
func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "nano \"test\"", cmdStr)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
"",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "which", name)
|
||||
return secureexec.Command("exit", "1")
|
||||
},
|
||||
func(env string) string {
|
||||
if env == "VISUAL" {
|
||||
return "nano"
|
||||
}
|
||||
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
"",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "which", name)
|
||||
return secureexec.Command("exit", "1")
|
||||
},
|
||||
func(env string) string {
|
||||
if env == "EDITOR" {
|
||||
return "emacs"
|
||||
}
|
||||
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "emacs \"test\"", cmdStr)
|
||||
},
|
||||
},
|
||||
{
|
||||
"test",
|
||||
"",
|
||||
func(name string, arg ...string) *exec.Cmd {
|
||||
assert.Equal(t, "which", name)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "vi \"test\"", cmdStr)
|
||||
},
|
||||
},
|
||||
{
|
||||
"file/with space",
|
||||
"",
|
||||
func(name string, args ...string) *exec.Cmd {
|
||||
assert.Equal(t, "which", name)
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(env string) string {
|
||||
return ""
|
||||
},
|
||||
func(cf string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmdStr string, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "vi \"file/with space\"", cmdStr)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.Config.GetUserConfig().OS.EditCommand = s.configEditCommand
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
gitCmd.OSCommand.Getenv = s.getenv
|
||||
gitCmd.getGitConfigValue = s.getGitConfigValue
|
||||
s.test(gitCmd.EditFileCmdStr(s.filename))
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,6 @@ type GitCommand struct {
|
||||
Tr *i18n.TranslationSet
|
||||
Config config.AppConfigurer
|
||||
getGitConfigValue func(string) (string, error)
|
||||
removeFile func(string) error
|
||||
DotGitDir string
|
||||
onSuccessfulContinue func() error
|
||||
PatchManager *patch.PatchManager
|
||||
@@ -75,7 +74,6 @@ func NewGitCommand(log *logrus.Entry, osCommand *oscommands.OSCommand, tr *i18n.
|
||||
Repo: repo,
|
||||
Config: config,
|
||||
getGitConfigValue: getGitConfigValue,
|
||||
removeFile: os.RemoveAll,
|
||||
DotGitDir: dotGitDir,
|
||||
PushToCurrent: pushToCurrent,
|
||||
}
|
||||
@@ -85,6 +83,27 @@ func NewGitCommand(log *logrus.Entry, osCommand *oscommands.OSCommand, tr *i18n.
|
||||
return gitCommand, nil
|
||||
}
|
||||
|
||||
func (c *GitCommand) WithSpan(span string) *GitCommand {
|
||||
// sometimes .WithSpan(span) will be called where span actually is empty, in
|
||||
// which case we don't need to log anything so we can just return early here
|
||||
// with the original struct
|
||||
if span == "" {
|
||||
return c
|
||||
}
|
||||
|
||||
newGitCommand := &GitCommand{}
|
||||
*newGitCommand = *c
|
||||
newGitCommand.OSCommand = c.OSCommand.WithSpan(span)
|
||||
|
||||
// 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
|
||||
// we've called .WithSpan() ahead of time with the new span value
|
||||
newGitCommand.PatchManager.ApplyPatch = newGitCommand.ApplyPatch
|
||||
|
||||
return newGitCommand
|
||||
}
|
||||
|
||||
func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir func(string) error) error {
|
||||
gitDir := env.GetGitDirEnv()
|
||||
if gitDir != "" {
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package commands
|
||||
|
||||
// Conflict : A git conflict with a start middle and end corresponding to line
|
||||
// numbers in the file where the conflict bars appear
|
||||
type Conflict struct {
|
||||
Start int
|
||||
Middle int
|
||||
End int
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
122
pkg/commands/loading_files_test.go
Normal file
122
pkg/commands/loading_files_test.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandGetStatusFiles is a function.
|
||||
func TestGitCommandGetStatusFiles(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func([]*models.File)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"No files found",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(files []*models.File) {
|
||||
assert.Len(t, files, 0)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Several files found",
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return secureexec.Command(
|
||||
"echo",
|
||||
"MM file1.txt\nA file3.txt\nAM file2.txt\n?? file4.txt\nUU file5.txt",
|
||||
)
|
||||
},
|
||||
func(files []*models.File) {
|
||||
assert.Len(t, files, 5)
|
||||
|
||||
expected := []*models.File{
|
||||
{
|
||||
Name: "file1.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Added: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "MM file1.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "MM",
|
||||
},
|
||||
{
|
||||
Name: "file3.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: false,
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "A file3.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "A ",
|
||||
},
|
||||
{
|
||||
Name: "file2.txt",
|
||||
HasStagedChanges: true,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "AM file2.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "AM",
|
||||
},
|
||||
{
|
||||
Name: "file4.txt",
|
||||
HasStagedChanges: false,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: false,
|
||||
Added: true,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: false,
|
||||
HasInlineMergeConflicts: false,
|
||||
DisplayString: "?? file4.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "??",
|
||||
},
|
||||
{
|
||||
Name: "file5.txt",
|
||||
HasStagedChanges: false,
|
||||
HasUnstagedChanges: true,
|
||||
Tracked: true,
|
||||
Added: false,
|
||||
Deleted: false,
|
||||
HasMergeConflicts: true,
|
||||
HasInlineMergeConflicts: true,
|
||||
DisplayString: "UU file5.txt",
|
||||
Type: "other",
|
||||
ShortStatus: "UU",
|
||||
},
|
||||
}
|
||||
|
||||
assert.EqualValues(t, expected, files)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
|
||||
s.test(gitCmd.GetStatusFiles(GetStatusFileOptions{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
61
pkg/commands/loading_stash_test.go
Normal file
61
pkg/commands/loading_stash_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandGetStashEntries is a function.
|
||||
func TestGitCommandGetStashEntries(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func([]*models.StashEntry)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"No stash entries found",
|
||||
func(string, ...string) *exec.Cmd {
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
func(entries []*models.StashEntry) {
|
||||
assert.Len(t, entries, 0)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Several stash entries found",
|
||||
func(string, ...string) *exec.Cmd {
|
||||
return secureexec.Command("echo", "WIP on add-pkg-commands-test: 55c6af2 increase parallel build\nWIP on master: bb86a3f update github template")
|
||||
},
|
||||
func(entries []*models.StashEntry) {
|
||||
expected := []*models.StashEntry{
|
||||
{
|
||||
Index: 0,
|
||||
Name: "WIP on add-pkg-commands-test: 55c6af2 increase parallel build",
|
||||
},
|
||||
{
|
||||
Index: 1,
|
||||
Name: "WIP on master: bb86a3f update github template",
|
||||
},
|
||||
}
|
||||
|
||||
assert.Len(t, entries, 2)
|
||||
assert.EqualValues(t, expected, entries)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
|
||||
s.test(gitCmd.GetStashEntries(""))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,16 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
const semverRegex = `v?((\d+\.?)+)([^\d]?.*)`
|
||||
|
||||
func convertToInt(s string) int {
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func (c *GitCommand) GetTags() ([]*models.Tag, error) {
|
||||
// get remote branches
|
||||
remoteBranchesStr, err := c.OSCommand.RunCommandWithOutput(`git tag --list`)
|
||||
// get remote branches, sorted by creation date (descending)
|
||||
// see: https://git-scm.com/docs/git-tag#Documentation/git-tag.txt---sortltkeygt
|
||||
remoteBranchesStr, err := c.OSCommand.RunCommandWithOutput(`git tag --list --sort=-creatordate`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -37,52 +25,10 @@ func (c *GitCommand) GetTags() ([]*models.Tag, error) {
|
||||
// first step is to get our remotes from go-git
|
||||
tags := make([]*models.Tag, len(split))
|
||||
for i, tagName := range split {
|
||||
|
||||
tags[i] = &models.Tag{
|
||||
Name: tagName,
|
||||
}
|
||||
}
|
||||
|
||||
// now lets sort our tags by name numerically
|
||||
re := regexp.MustCompile(semverRegex)
|
||||
|
||||
// the reason this is complicated is because we're both sorting alphabetically
|
||||
// and when we're dealing with semver strings
|
||||
sort.Slice(tags, func(i, j int) bool {
|
||||
a := tags[i].Name
|
||||
b := tags[j].Name
|
||||
|
||||
matchA := re.FindStringSubmatch(a)
|
||||
matchB := re.FindStringSubmatch(b)
|
||||
|
||||
if len(matchA) > 0 && len(matchB) > 0 {
|
||||
numbersA := strings.Split(matchA[1], ".")
|
||||
numbersB := strings.Split(matchB[1], ".")
|
||||
k := 0
|
||||
for {
|
||||
if len(numbersA) == k && len(numbersB) == k {
|
||||
break
|
||||
}
|
||||
if len(numbersA) == k {
|
||||
return true
|
||||
}
|
||||
if len(numbersB) == k {
|
||||
return false
|
||||
}
|
||||
if convertToInt(numbersA[k]) < convertToInt(numbersB[k]) {
|
||||
return true
|
||||
}
|
||||
if convertToInt(numbersA[k]) > convertToInt(numbersB[k]) {
|
||||
return false
|
||||
}
|
||||
k++
|
||||
}
|
||||
|
||||
return strings.ToLower(matchA[3]) < strings.ToLower(matchB[3])
|
||||
}
|
||||
|
||||
return strings.ToLower(a) < strings.ToLower(b)
|
||||
})
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
// NOTE: If the return data is empty it won't written anything to stdin
|
||||
func RunCommandWithOutputLiveWrapper(c *OSCommand, command string, output func(string) string) error {
|
||||
c.Log.WithField("command", command).Info("RunCommand")
|
||||
c.LogCommand(command, true)
|
||||
cmd := c.ExecutableFromString(command)
|
||||
cmd.Env = append(cmd.Env, "LANG=en_US.UTF-8", "LC_ALL=en_US.UTF-8")
|
||||
|
||||
|
||||
@@ -40,6 +40,44 @@ type OSCommand struct {
|
||||
Command func(string, ...string) *exec.Cmd
|
||||
BeforeExecuteCmd func(*exec.Cmd)
|
||||
Getenv func(string) string
|
||||
|
||||
// callback to run before running a command, i.e. for the purposes of logging
|
||||
onRunCommand func(CmdLogEntry)
|
||||
|
||||
// something like 'Staging File': allows us to group cmd logs under a single title
|
||||
CmdLogSpan string
|
||||
|
||||
removeFile func(string) error
|
||||
}
|
||||
|
||||
// TODO: make these fields private
|
||||
type CmdLogEntry struct {
|
||||
// e.g. 'git commit -m "haha"'
|
||||
cmdStr string
|
||||
// Span is something like 'Staging File'. Multiple commands can be grouped under the same
|
||||
// span
|
||||
span string
|
||||
|
||||
// sometimes our command is direct like 'git commit', and sometimes it's a
|
||||
// command to remove a file but through Go's standard library rather than the
|
||||
// command line
|
||||
commandLine bool
|
||||
}
|
||||
|
||||
func (e CmdLogEntry) GetCmdStr() string {
|
||||
return e.cmdStr
|
||||
}
|
||||
|
||||
func (e CmdLogEntry) GetSpan() string {
|
||||
return e.span
|
||||
}
|
||||
|
||||
func (e CmdLogEntry) GetCommandLine() bool {
|
||||
return e.commandLine
|
||||
}
|
||||
|
||||
func NewCmdLogEntry(cmdStr string, span string, commandLine bool) CmdLogEntry {
|
||||
return CmdLogEntry{cmdStr: cmdStr, span: span, commandLine: commandLine}
|
||||
}
|
||||
|
||||
// NewOSCommand os command runner
|
||||
@@ -51,15 +89,51 @@ func NewOSCommand(log *logrus.Entry, config config.AppConfigurer) *OSCommand {
|
||||
Command: secureexec.Command,
|
||||
BeforeExecuteCmd: func(*exec.Cmd) {},
|
||||
Getenv: os.Getenv,
|
||||
removeFile: os.RemoveAll,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *OSCommand) WithSpan(span string) *OSCommand {
|
||||
// sometimes .WithSpan(span) will be called where span actually is empty, in
|
||||
// which case we don't need to log anything so we can just return early here
|
||||
// with the original struct
|
||||
if span == "" {
|
||||
return c
|
||||
}
|
||||
|
||||
newOSCommand := &OSCommand{}
|
||||
*newOSCommand = *c
|
||||
newOSCommand.CmdLogSpan = span
|
||||
return newOSCommand
|
||||
}
|
||||
|
||||
func (c *OSCommand) LogExecCmd(cmd *exec.Cmd) {
|
||||
c.LogCommand(strings.Join(cmd.Args, " "), true)
|
||||
}
|
||||
|
||||
func (c *OSCommand) LogCommand(cmdStr string, commandLine bool) {
|
||||
c.Log.WithField("command", cmdStr).Info("RunCommand")
|
||||
|
||||
if c.onRunCommand != nil && c.CmdLogSpan != "" {
|
||||
c.onRunCommand(NewCmdLogEntry(cmdStr, c.CmdLogSpan, commandLine))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -69,7 +143,7 @@ type RunCommandOptions struct {
|
||||
}
|
||||
|
||||
func (c *OSCommand) RunCommandWithOutputWithOptions(command string, options RunCommandOptions) (string, error) {
|
||||
c.Log.WithField("command", command).Info("RunCommand")
|
||||
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
|
||||
@@ -94,8 +168,8 @@ func (c *OSCommand) RunCommandWithOutput(formatString string, formatArgs ...inte
|
||||
if formatArgs != nil {
|
||||
command = fmt.Sprintf(formatString, formatArgs...)
|
||||
}
|
||||
c.Log.WithField("command", command).Info("RunCommand")
|
||||
cmd := c.ExecutableFromString(command)
|
||||
c.LogExecCmd(cmd)
|
||||
output, err := sanitisedCommandOutput(cmd.CombinedOutput())
|
||||
if err != nil {
|
||||
c.Log.WithField("command", command).Error(output)
|
||||
@@ -103,20 +177,9 @@ func (c *OSCommand) RunCommandWithOutput(formatString string, formatArgs ...inte
|
||||
return output, err
|
||||
}
|
||||
|
||||
func (c *OSCommand) CatFile(filename string) (string, error) {
|
||||
arr := append(c.Platform.CatCmd, filename)
|
||||
cmdStr := strings.Join(arr, " ")
|
||||
c.Log.WithField("command", cmdStr).Info("Cat")
|
||||
cmd := c.Command(arr[0], arr[1:]...)
|
||||
output, err := sanitisedCommandOutput(cmd.CombinedOutput())
|
||||
if err != nil {
|
||||
c.Log.WithField("command", cmdStr).Error(output)
|
||||
}
|
||||
return output, err
|
||||
}
|
||||
|
||||
// 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())
|
||||
}
|
||||
@@ -154,6 +217,18 @@ func (c *OSCommand) RunCommandWithOutputLive(command string, output func(string)
|
||||
return RunCommandWithOutputLiveWrapper(c, command, output)
|
||||
}
|
||||
|
||||
func (c *OSCommand) CatFile(filename string) (string, error) {
|
||||
arr := append(c.Platform.CatCmd, filename)
|
||||
cmdStr := strings.Join(arr, " ")
|
||||
c.Log.WithField("command", cmdStr).Info("Cat")
|
||||
cmd := c.Command(arr[0], arr[1:]...)
|
||||
output, err := sanitisedCommandOutput(cmd.CombinedOutput())
|
||||
if err != nil {
|
||||
c.Log.WithField("command", cmdStr).Error(output)
|
||||
}
|
||||
return output, err
|
||||
}
|
||||
|
||||
// DetectUnamePass detect a username / password / passphrase question in a command
|
||||
// promptUserForCredential is a function that gets executed when this function detect you need to fillin a password or passphrase
|
||||
// The promptUserForCredential argument will be "username", "password" or "passphrase" and expects the user's password/passphrase or username back
|
||||
@@ -190,9 +265,9 @@ func (c *OSCommand) RunCommand(formatString string, formatArgs ...interface{}) e
|
||||
// RunShellCommand runs shell commands i.e. 'sh -c <command>'. Good for when you
|
||||
// need access to the shell
|
||||
func (c *OSCommand) RunShellCommand(command string) error {
|
||||
c.Log.WithField("command", command).Info("RunShellCommand")
|
||||
|
||||
cmd := c.Command(c.Platform.Shell, c.Platform.ShellArg, command)
|
||||
c.LogExecCmd(cmd)
|
||||
|
||||
_, err := sanitisedCommandOutput(cmd.CombinedOutput())
|
||||
|
||||
return err
|
||||
@@ -237,6 +312,7 @@ func (c *OSCommand) OpenFile(filename string) error {
|
||||
|
||||
// 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
|
||||
templateValues := map[string]string{
|
||||
"link": c.Quote(link),
|
||||
@@ -254,6 +330,7 @@ func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *ex
|
||||
if cmd != nil {
|
||||
cmd.Env = append(os.Environ(), "GIT_OPTIONAL_LOCKS=0")
|
||||
}
|
||||
c.LogExecCmd(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -279,6 +356,7 @@ func (c *OSCommand) Quote(message string) string {
|
||||
|
||||
// AppendLineToFile adds a new line in file
|
||||
func (c *OSCommand) AppendLineToFile(filename, line string) error {
|
||||
c.LogCommand(fmt.Sprintf("Appending '%s' to file '%s'", line, filename), false)
|
||||
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
return utils.WrapError(err)
|
||||
@@ -299,6 +377,7 @@ func (c *OSCommand) CreateTempFile(filename, content string) (string, error) {
|
||||
c.Log.Error(err)
|
||||
return "", utils.WrapError(err)
|
||||
}
|
||||
c.LogCommand(fmt.Sprintf("Creating temp file '%s'", tmpfile.Name()), false)
|
||||
|
||||
if _, err := tmpfile.WriteString(content); err != nil {
|
||||
c.Log.Error(err)
|
||||
@@ -314,6 +393,7 @@ func (c *OSCommand) CreateTempFile(filename, content string) (string, error) {
|
||||
|
||||
// CreateFileWithContent creates a file with the given content
|
||||
func (c *OSCommand) CreateFileWithContent(path string, content string) error {
|
||||
c.LogCommand(fmt.Sprintf("Creating file '%s'", path), false)
|
||||
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
|
||||
c.Log.Error(err)
|
||||
return err
|
||||
@@ -329,6 +409,7 @@ func (c *OSCommand) CreateFileWithContent(path string, content string) error {
|
||||
|
||||
// Remove removes a file or directory at the specified path
|
||||
func (c *OSCommand) Remove(filename string) error {
|
||||
c.LogCommand(fmt.Sprintf("Removing '%s'", filename), false)
|
||||
err := os.RemoveAll(filename)
|
||||
return utils.WrapError(err)
|
||||
}
|
||||
@@ -349,6 +430,7 @@ func (c *OSCommand) FileExists(path string) (bool, error) {
|
||||
// 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)
|
||||
@@ -372,12 +454,16 @@ func (c *OSCommand) GetLazygitPath() string {
|
||||
|
||||
// PipeCommands runs a heap of commands and pipes their inputs/outputs together like A | B | C
|
||||
func (c *OSCommand) PipeCommands(commandStrings ...string) error {
|
||||
|
||||
cmds := make([]*exec.Cmd, len(commandStrings))
|
||||
|
||||
logCmdStr := ""
|
||||
for i, str := range commandStrings {
|
||||
if i > 0 {
|
||||
logCmdStr += " | "
|
||||
}
|
||||
logCmdStr += str
|
||||
cmds[i] = c.ExecutableFromString(str)
|
||||
}
|
||||
c.LogCommand(logCmdStr, true)
|
||||
|
||||
for i := 0; i < len(cmds)-1; i++ {
|
||||
stdout, err := cmds[i].StdoutPipe()
|
||||
@@ -468,5 +554,12 @@ func RunLineOutputCmd(cmd *exec.Cmd, onLine func(line string) (bool, error)) err
|
||||
}
|
||||
|
||||
func (c *OSCommand) CopyToClipboard(str string) error {
|
||||
c.LogCommand(fmt.Sprintf("Copying '%s' to clipboard", utils.TruncateWithEllipsis(str, 40)), false)
|
||||
return clipboard.WriteAll(str)
|
||||
}
|
||||
|
||||
func (c *OSCommand) RemoveFile(path string) error {
|
||||
c.LogCommand(fmt.Sprintf("Deleting path '%s'", path), false)
|
||||
|
||||
return c.removeFile(path)
|
||||
}
|
||||
|
||||
@@ -182,11 +182,8 @@ func (p *PatchManager) RenderPatchForFile(filename string, plain bool, reverse b
|
||||
if plain {
|
||||
return patch
|
||||
}
|
||||
parser, err := NewPatchParser(p.Log, patch)
|
||||
if err != nil {
|
||||
// swallowing for now
|
||||
return ""
|
||||
}
|
||||
parser := NewPatchParser(p.Log, patch)
|
||||
|
||||
// not passing included lines because we don't want to see them in the secondary panel
|
||||
return parser.Render(-1, -1, nil)
|
||||
}
|
||||
|
||||
@@ -39,11 +39,8 @@ type PatchParser struct {
|
||||
}
|
||||
|
||||
// NewPatchParser builds a new branch list builder
|
||||
func NewPatchParser(log *logrus.Entry, patch string) (*PatchParser, error) {
|
||||
hunkStarts, stageableLines, patchLines, err := parsePatch(patch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func NewPatchParser(log *logrus.Entry, patch string) *PatchParser {
|
||||
hunkStarts, stageableLines, patchLines := parsePatch(patch)
|
||||
|
||||
patchHunks := GetHunksFromDiff(patch)
|
||||
|
||||
@@ -53,7 +50,7 @@ func NewPatchParser(log *logrus.Entry, patch string) (*PatchParser, error) {
|
||||
StageableLines: stageableLines,
|
||||
PatchLines: patchLines,
|
||||
PatchHunks: patchHunks,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// GetHunkContainingLine takes a line index and an offset and finds the hunk
|
||||
@@ -139,7 +136,7 @@ func coloredString(colorAttr color.Attribute, str string, selected bool, include
|
||||
return utils.ColoredStringDirect(str[:1], clIncluded) + utils.ColoredStringDirect(str[1:], cl)
|
||||
}
|
||||
|
||||
func parsePatch(patch string) ([]int, []int, []*PatchLine, error) {
|
||||
func parsePatch(patch string) ([]int, []int, []*PatchLine) {
|
||||
lines := strings.Split(patch, "\n")
|
||||
hunkStarts := []int{}
|
||||
stageableLines := []int{}
|
||||
@@ -185,7 +182,7 @@ func parsePatch(patch string) ([]int, []int, []*PatchLine, error) {
|
||||
}
|
||||
patchLines[index] = &PatchLine{Kind: lineKind, Content: line}
|
||||
}
|
||||
return hunkStarts, stageableLines, patchLines, nil
|
||||
return hunkStarts, stageableLines, patchLines
|
||||
}
|
||||
|
||||
// Render returns the coloured string of the diff with any selected lines highlighted
|
||||
|
||||
@@ -23,7 +23,7 @@ func (c *GitCommand) DeletePatchesFromCommit(commits []*models.Commit, commitInd
|
||||
}
|
||||
|
||||
// time to amend the selected commit
|
||||
if _, err := c.AmendHead(); err != nil {
|
||||
if err := c.AmendHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
|
||||
}
|
||||
|
||||
// amend the destination commit
|
||||
if _, err := c.AmendHead(); err != nil {
|
||||
if err := c.AmendHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
|
||||
// we can make this GPG thing possible it just means we need to do this in two parts:
|
||||
// one where we handle the possibility of a credential request, and the other
|
||||
// where we continue the rebase
|
||||
if c.usingGpg() {
|
||||
if c.UsingGpg() {
|
||||
return errors.New(c.Tr.DisabledForGPG)
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
|
||||
}
|
||||
|
||||
// amend the source commit
|
||||
if _, err := c.AmendHead(); err != nil {
|
||||
if err := c.AmendHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
|
||||
}
|
||||
|
||||
// amend the destination commit
|
||||
if _, err := c.AmendHead(); err != nil {
|
||||
if err := c.AmendHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
|
||||
return c.GenericMergeOrRebaseAction("rebase", "continue")
|
||||
}
|
||||
|
||||
func (c *GitCommand) PullPatchIntoIndex(commits []*models.Commit, commitIdx int, p *patch.PatchManager, stash bool) error {
|
||||
func (c *GitCommand) MovePatchIntoIndex(commits []*models.Commit, commitIdx int, p *patch.PatchManager, stash bool) error {
|
||||
if stash {
|
||||
if err := c.StashSave(c.Tr.StashPrefix + commits[commitIdx].Sha); err != nil {
|
||||
return err
|
||||
@@ -158,7 +158,7 @@ func (c *GitCommand) PullPatchIntoIndex(commits []*models.Commit, commitIdx int,
|
||||
}
|
||||
|
||||
// amend the commit
|
||||
if _, err := c.AmendHead(); err != nil {
|
||||
if err := c.AmendHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ func (c *GitCommand) PullPatchIntoNewCommit(commits []*models.Commit, commitIdx
|
||||
}
|
||||
|
||||
// amend the commit
|
||||
if _, err := c.AmendHead(); err != nil {
|
||||
if err := c.AmendHead(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ func (c *GitCommand) PullPatchIntoNewCommit(commits []*models.Commit, commitIdx
|
||||
|
||||
head_message, _ := c.GetHeadCommitMessage()
|
||||
new_message := fmt.Sprintf("Split from \"%s\"", head_message)
|
||||
_, err := c.Commit(new_message, "")
|
||||
err := c.OSCommand.RunCommand(c.CommitCmdStr(new_message, ""))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -90,23 +90,23 @@ func NewPullRequest(gitCommand *GitCommand) *PullRequest {
|
||||
}
|
||||
|
||||
// Create opens link to new pull request in browser
|
||||
func (pr *PullRequest) Create(branch *models.Branch) error {
|
||||
func (pr *PullRequest) Create(branch *models.Branch) (string, error) {
|
||||
pullRequestURL, err := pr.getPullRequestURL(branch)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
return pr.GitCommand.OSCommand.OpenLink(pullRequestURL)
|
||||
return pullRequestURL, pr.GitCommand.OSCommand.OpenLink(pullRequestURL)
|
||||
}
|
||||
|
||||
// CopyURL copies the pull request URL to the clipboard
|
||||
func (pr *PullRequest) CopyURL(branch *models.Branch) error {
|
||||
func (pr *PullRequest) CopyURL(branch *models.Branch) (string, error) {
|
||||
pullRequestURL, err := pr.getPullRequestURL(branch)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
return pr.GitCommand.OSCommand.CopyToClipboard(pullRequestURL)
|
||||
return pullRequestURL, pr.GitCommand.OSCommand.CopyToClipboard(pullRequestURL)
|
||||
}
|
||||
|
||||
func (pr *PullRequest) getPullRequestURL(branch *models.Branch) (string, error) {
|
||||
|
||||
@@ -51,7 +51,7 @@ func TestCreatePullRequest(t *testing.T) {
|
||||
branch *models.Branch
|
||||
remoteUrl string
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(err error)
|
||||
test func(url string, err error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
@@ -71,8 +71,9 @@ func TestCreatePullRequest(t *testing.T) {
|
||||
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/profile-page&t=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(err error) {
|
||||
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)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -91,8 +92,9 @@ func TestCreatePullRequest(t *testing.T) {
|
||||
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?source=feature/events&t=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(err error) {
|
||||
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)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -111,8 +113,9 @@ func TestCreatePullRequest(t *testing.T) {
|
||||
assert.Equal(t, args, []string{"https://github.com/peter/calculator/compare/feature/sum-operation?expand=1"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(err error) {
|
||||
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)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -131,8 +134,9 @@ func TestCreatePullRequest(t *testing.T) {
|
||||
assert.Equal(t, args, []string{"https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"})
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(err error) {
|
||||
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)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -144,7 +148,7 @@ func TestCreatePullRequest(t *testing.T) {
|
||||
command: func(cmd string, args ...string) *exec.Cmd {
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
test: func(err error) {
|
||||
test: func(url string, err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
|
||||
@@ -77,6 +77,8 @@ func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string
|
||||
gitSequenceEditor := ex
|
||||
if todo == "" {
|
||||
gitSequenceEditor = "true"
|
||||
} else {
|
||||
c.OSCommand.LogCommand(fmt.Sprintf("Creating TODO file for interactive rebase: \n\n%s", todo), false)
|
||||
}
|
||||
|
||||
cmd.Env = os.Environ()
|
||||
@@ -211,7 +213,7 @@ func (c *GitCommand) BeginInteractiveRebaseForCommit(commits []*models.Commit, c
|
||||
// we can make this GPG thing possible it just means we need to do this in two parts:
|
||||
// one where we handle the possibility of a credential request, and the other
|
||||
// where we continue the rebase
|
||||
if c.usingGpg() {
|
||||
if c.UsingGpg() {
|
||||
return errors.New(c.Tr.DisabledForGPG)
|
||||
}
|
||||
|
||||
|
||||
96
pkg/commands/rebasing_test.go
Normal file
96
pkg/commands/rebasing_test.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/test"
|
||||
"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
|
||||
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) {
|
||||
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) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
gitCmd := NewDummyGitCommand()
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
s.test(gitCmd.RebaseBranch(s.arg))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 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",
|
||||
)
|
||||
})
|
||||
|
||||
_ = cmd.runSkipEditorCommand("true")
|
||||
}
|
||||
@@ -21,7 +21,7 @@ func (c *GitCommand) ShowStashEntryCmdStr(index int) string {
|
||||
// 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 {
|
||||
return err
|
||||
}
|
||||
|
||||
35
pkg/commands/stash_entries_test.go
Normal file
35
pkg/commands/stash_entries_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"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")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.StashDo(1, "drop"))
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
assert.NoError(t, gitCmd.StashSave("A stash message"))
|
||||
}
|
||||
@@ -2,23 +2,8 @@ package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// usingGpg tells us whether the user has gpg enabled so that we can know
|
||||
// whether we need to run a subprocess to allow them to enter their password
|
||||
func (c *GitCommand) usingGpg() bool {
|
||||
overrideGpg := c.Config.GetUserConfig().Git.OverrideGpg
|
||||
if overrideGpg {
|
||||
return false
|
||||
}
|
||||
|
||||
gpgsign := c.GetConfigValue("commit.gpgsign")
|
||||
value := strings.ToLower(gpgsign)
|
||||
|
||||
return value == "true" || value == "1" || value == "yes" || value == "on"
|
||||
}
|
||||
|
||||
// Push pushes to a branch
|
||||
func (c *GitCommand) Push(branchName string, force bool, upstream string, args string, promptUserForCredential func(string) string) error {
|
||||
followTagsFlag := "--follow-tags"
|
||||
|
||||
98
pkg/commands/sync_test.go
Normal file
98
pkg/commands/sync_test.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGitCommandPush is a function.
|
||||
func TestGitCommandPush(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
getGitConfigValue func(string) (string, error)
|
||||
command func(string, ...string) *exec.Cmd
|
||||
forcePush bool
|
||||
test func(error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Push with force disabled, follow-tags on",
|
||||
func(string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push", "--follow-tags"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
false,
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with force enabled, follow-tags on",
|
||||
func(string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push", "--follow-tags", "--force-with-lease"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
true,
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with force disabled, follow-tags off",
|
||||
func(string) (string, error) {
|
||||
return "false", nil
|
||||
},
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push"}, args)
|
||||
|
||||
return secureexec.Command("echo")
|
||||
},
|
||||
false,
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Push with an error occurring, follow-tags on",
|
||||
func(string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
assert.EqualValues(t, "git", cmd)
|
||||
assert.EqualValues(t, []string{"push", "--follow-tags"}, args)
|
||||
return secureexec.Command("test")
|
||||
},
|
||||
false,
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command = s.command
|
||||
gitCmd.getGitConfigValue = s.getGitConfigValue
|
||||
err := gitCmd.Push("test", s.forcePush, "", "", func(passOrUname string) string {
|
||||
return "\n"
|
||||
})
|
||||
s.test(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ package config
|
||||
// GetPlatformDefaultConfig gets the defaults for the platform
|
||||
func GetPlatformDefaultConfig() OSConfig {
|
||||
return OSConfig{
|
||||
EditCommand: ``,
|
||||
OpenCommand: "open {{filename}}",
|
||||
OpenLinkCommand: "open {{link}}",
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package config
|
||||
// GetPlatformDefaultConfig gets the defaults for the platform
|
||||
func GetPlatformDefaultConfig() OSConfig {
|
||||
return OSConfig{
|
||||
EditCommand: ``,
|
||||
OpenCommand: `sh -c "xdg-open {{filename}} >/dev/null"`,
|
||||
OpenLinkCommand: `sh -c "xdg-open {{link}} >/dev/null"`,
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package config
|
||||
// GetPlatformDefaultConfig gets the defaults for the platform
|
||||
func GetPlatformDefaultConfig() OSConfig {
|
||||
return OSConfig{
|
||||
EditCommand: ``,
|
||||
OpenCommand: `cmd /c "start "" {{filename}}"`,
|
||||
OpenLinkCommand: `cmd /c "start "" {{link}}"`,
|
||||
}
|
||||
|
||||
@@ -36,6 +36,9 @@ type GuiConfig struct {
|
||||
CommitLength CommitLengthConfig `yaml:"commitLength"`
|
||||
SkipNoStagedFilesWarning bool `yaml:"skipNoStagedFilesWarning"`
|
||||
ShowFileTree bool `yaml:"showFileTree"`
|
||||
ShowRandomTip bool `yaml:"showRandomTip"`
|
||||
ShowCommandLog bool `yaml:"showCommandLog"`
|
||||
CommandLogSize int `yaml:"commandLogSize"`
|
||||
}
|
||||
|
||||
type ThemeConfig struct {
|
||||
@@ -101,6 +104,7 @@ type KeybindingConfig struct {
|
||||
Submodules KeybindingSubmodulesConfig `yaml:"submodules"`
|
||||
}
|
||||
|
||||
// damn looks like we have some inconsistencies here with -alt and -alt1
|
||||
type KeybindingUniversalConfig struct {
|
||||
Quit string `yaml:"quit"`
|
||||
QuitAlt1 string `yaml:"quit-alt1"`
|
||||
@@ -119,6 +123,8 @@ type KeybindingUniversalConfig struct {
|
||||
NextBlock string `yaml:"nextBlock"`
|
||||
PrevBlockAlt string `yaml:"prevBlock-alt"`
|
||||
NextBlockAlt string `yaml:"nextBlock-alt"`
|
||||
NextBlockAlt2 string `yaml:"nextBlock-alt2"`
|
||||
PrevBlockAlt2 string `yaml:"prevBlock-alt2"`
|
||||
NextMatch string `yaml:"nextMatch"`
|
||||
PrevMatch string `yaml:"prevMatch"`
|
||||
StartSearch string `yaml:"startSearch"`
|
||||
@@ -154,8 +160,10 @@ type KeybindingUniversalConfig struct {
|
||||
DiffingMenu string `yaml:"diffingMenu"`
|
||||
DiffingMenuAlt string `yaml:"diffingMenu-alt"`
|
||||
CopyToClipboard string `yaml:"copyToClipboard"`
|
||||
OpenRecentRepos string `yaml:"openRecentRepos"`
|
||||
SubmitEditorText string `yaml:"submitEditorText"`
|
||||
AppendNewline string `yaml:"appendNewline"`
|
||||
ExtrasMenu string `yaml:"extrasMenu"`
|
||||
}
|
||||
|
||||
type KeybindingStatusConfig struct {
|
||||
@@ -177,6 +185,7 @@ type KeybindingFilesConfig struct {
|
||||
ViewResetOptions string `yaml:"viewResetOptions"`
|
||||
Fetch string `yaml:"fetch"`
|
||||
ToggleTreeView string `yaml:"toggleTreeView"`
|
||||
OpenMergeTool string `yaml:"openMergeTool"`
|
||||
}
|
||||
|
||||
type KeybindingBranchesConfig struct {
|
||||
@@ -239,6 +248,9 @@ type KeybindingSubmodulesConfig struct {
|
||||
|
||||
// OSConfig contains config on the level of the os
|
||||
type OSConfig struct {
|
||||
// EditCommand is the command for editing a file
|
||||
EditCommand string `yaml:"editCommand,omitempty"`
|
||||
|
||||
// OpenCommand is the command for opening a file
|
||||
OpenCommand string `yaml:"openCommand,omitempty"`
|
||||
|
||||
@@ -294,6 +306,10 @@ func GetDefaultConfig() *UserConfig {
|
||||
},
|
||||
CommitLength: CommitLengthConfig{Show: true},
|
||||
SkipNoStagedFilesWarning: false,
|
||||
ShowCommandLog: true,
|
||||
ShowFileTree: false,
|
||||
ShowRandomTip: true,
|
||||
CommandLogSize: 8,
|
||||
},
|
||||
Git: GitConfig{
|
||||
Paging: PagingConfig{
|
||||
@@ -345,6 +361,8 @@ func GetDefaultConfig() *UserConfig {
|
||||
NextBlock: "<right>",
|
||||
PrevBlockAlt: "h",
|
||||
NextBlockAlt: "l",
|
||||
PrevBlockAlt2: "<backtab>",
|
||||
NextBlockAlt2: "<tab>",
|
||||
NextMatch: "n",
|
||||
PrevMatch: "N",
|
||||
StartSearch: "/",
|
||||
@@ -358,6 +376,7 @@ func GetDefaultConfig() *UserConfig {
|
||||
New: "n",
|
||||
Edit: "e",
|
||||
OpenFile: "o",
|
||||
OpenRecentRepos: "<c-r>",
|
||||
ScrollUpMain: "<pgup>",
|
||||
ScrollDownMain: "<pgdown>",
|
||||
ScrollUpMainAlt1: "K",
|
||||
@@ -382,6 +401,7 @@ func GetDefaultConfig() *UserConfig {
|
||||
CopyToClipboard: "<c-o>",
|
||||
SubmitEditorText: "<enter>",
|
||||
AppendNewline: "<a-enter>",
|
||||
ExtrasMenu: "@",
|
||||
},
|
||||
Status: KeybindingStatusConfig{
|
||||
CheckForUpdate: "u",
|
||||
@@ -401,6 +421,7 @@ func GetDefaultConfig() *UserConfig {
|
||||
ViewResetOptions: "D",
|
||||
Fetch: "f",
|
||||
ToggleTreeView: "`",
|
||||
OpenMergeTool: "M",
|
||||
},
|
||||
Branches: KeybindingBranchesConfig{
|
||||
CopyPullRequestURL: "<c-y>",
|
||||
|
||||
35
pkg/constants/links.go
Normal file
35
pkg/constants/links.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package constants
|
||||
|
||||
type Docs struct {
|
||||
CustomPagers string
|
||||
CustomCommands string
|
||||
CustomKeybindings string
|
||||
Keybindings string
|
||||
Undoing string
|
||||
Config string
|
||||
Tutorial string
|
||||
}
|
||||
|
||||
var Links = struct {
|
||||
Docs Docs
|
||||
Issues string
|
||||
Donate string
|
||||
Discussions string
|
||||
RepoUrl string
|
||||
Releases string
|
||||
}{
|
||||
RepoUrl: "https://github.com/jesseduffield/lazygit",
|
||||
Issues: "https://github.com/jesseduffield/lazygit/issues",
|
||||
Donate: "https://github.com/sponsors/jesseduffield",
|
||||
Discussions: "https://github.com/jesseduffield/lazygit/discussions",
|
||||
Releases: "https://github.com/jesseduffield/lazygit/releases",
|
||||
Docs: Docs{
|
||||
CustomPagers: "https://github.com/jesseduffield/lazygit/blob/master/docs/Custom_Pagers.md",
|
||||
CustomKeybindings: "https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md",
|
||||
CustomCommands: "https://github.com/jesseduffield/lazygit/wiki/Custom-Commands-Compendium",
|
||||
Keybindings: "https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings",
|
||||
Undoing: "https://github.com/jesseduffield/lazygit/blob/master/docs/Undoing.md",
|
||||
Config: "https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md",
|
||||
Tutorial: "https://youtu.be/VDXvbHZYeKY",
|
||||
},
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
const INFO_SECTION_PADDING = " "
|
||||
|
||||
func (gui *Gui) mainSectionChildren() []*boxlayout.Box {
|
||||
currentWindow := gui.currentWindow()
|
||||
|
||||
@@ -130,6 +132,24 @@ func (gui *Gui) splitMainPanelSideBySide() bool {
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) getExtrasWindowSize(screenHeight int) int {
|
||||
if !gui.ShowExtrasWindow {
|
||||
return 0
|
||||
}
|
||||
|
||||
var baseSize int
|
||||
if gui.currentStaticContext().GetKey() == COMMAND_LOG_CONTEXT_KEY {
|
||||
baseSize = 1000 // my way of saying 'fill the available space'
|
||||
} else if screenHeight < 40 {
|
||||
baseSize = 1
|
||||
} else {
|
||||
baseSize = gui.Config.GetUserConfig().Gui.CommandLogSize
|
||||
}
|
||||
|
||||
frameSize := 2
|
||||
return baseSize + frameSize
|
||||
}
|
||||
|
||||
func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions {
|
||||
width, height := gui.g.Size()
|
||||
|
||||
@@ -146,6 +166,8 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
|
||||
mainPanelsDirection = boxlayout.COLUMN
|
||||
}
|
||||
|
||||
extrasWindowSize := gui.getExtrasWindowSize(height)
|
||||
|
||||
root := &boxlayout.Box{
|
||||
Direction: boxlayout.ROW,
|
||||
Children: []*boxlayout.Box{
|
||||
@@ -159,9 +181,19 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
|
||||
ConditionalChildren: gui.sidePanelChildren,
|
||||
},
|
||||
{
|
||||
Direction: mainPanelsDirection,
|
||||
Direction: boxlayout.ROW,
|
||||
Weight: mainSectionWeight,
|
||||
Children: gui.mainSectionChildren(),
|
||||
Children: []*boxlayout.Box{
|
||||
{
|
||||
Direction: mainPanelsDirection,
|
||||
Children: gui.mainSectionChildren(),
|
||||
Weight: 1,
|
||||
},
|
||||
{
|
||||
Window: "extras",
|
||||
Size: extrasWindowSize,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
77
pkg/gui/basic_context.go
Normal file
77
pkg/gui/basic_context.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package gui
|
||||
|
||||
type BasicContext struct {
|
||||
OnFocus func() error
|
||||
OnFocusLost func() error
|
||||
OnRender func() error
|
||||
Kind ContextKind
|
||||
Key ContextKey
|
||||
ViewName string
|
||||
WindowName string
|
||||
OnGetOptionsMap func() map[string]string
|
||||
|
||||
ParentContext Context
|
||||
// we can't know on the calling end whether a Context is actually a nil value without reflection, so we're storing this flag here to tell us. There has got to be a better way around this
|
||||
hasParent bool
|
||||
}
|
||||
|
||||
func (c *BasicContext) GetOptionsMap() map[string]string {
|
||||
if c.OnGetOptionsMap != nil {
|
||||
return c.OnGetOptionsMap()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *BasicContext) SetParentContext(context Context) {
|
||||
c.ParentContext = context
|
||||
c.hasParent = true
|
||||
}
|
||||
|
||||
func (c *BasicContext) GetParentContext() (Context, bool) {
|
||||
return c.ParentContext, c.hasParent
|
||||
}
|
||||
|
||||
func (c *BasicContext) SetWindowName(windowName string) {
|
||||
c.WindowName = windowName
|
||||
}
|
||||
|
||||
func (c *BasicContext) GetWindowName() string {
|
||||
windowName := c.WindowName
|
||||
|
||||
if windowName != "" {
|
||||
return windowName
|
||||
}
|
||||
|
||||
// TODO: actually set this for everything so we don't default to the view name
|
||||
return c.ViewName
|
||||
}
|
||||
|
||||
func (c *BasicContext) HandleRender() error {
|
||||
if c.OnRender != nil {
|
||||
return c.OnRender()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *BasicContext) GetViewName() string {
|
||||
return c.ViewName
|
||||
}
|
||||
|
||||
func (c *BasicContext) HandleFocus() error {
|
||||
return c.OnFocus()
|
||||
}
|
||||
|
||||
func (c *BasicContext) HandleFocusLost() error {
|
||||
if c.OnFocusLost != nil {
|
||||
return c.OnFocusLost()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *BasicContext) GetKind() ContextKind {
|
||||
return c.Kind
|
||||
}
|
||||
|
||||
func (c *BasicContext) GetKey() ContextKey {
|
||||
return c.Key
|
||||
}
|
||||
@@ -96,6 +96,11 @@ func ArrangeWindows(root *Box, x0, y0, width, height int) map[string]Dimensions
|
||||
var boxSize int
|
||||
if child.isStatic() {
|
||||
boxSize = child.Size
|
||||
// assuming that only one static child can have a size greater than the
|
||||
// available space. In that case we just crop the size to what's available
|
||||
if boxSize > availableSize {
|
||||
boxSize = availableSize
|
||||
}
|
||||
} else {
|
||||
// TODO: consider more evenly distributing the remainder
|
||||
boxSize = unitSize * child.Weight
|
||||
|
||||
@@ -179,6 +179,27 @@ func TestArrangeWindows(t *testing.T) {
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Box with static child with size too large",
|
||||
&Box{Direction: COLUMN, Children: []*Box{{Size: 11, Window: "static"}, {Weight: 1, Window: "dynamic1"}, {Weight: 2, Window: "dynamic2"}}},
|
||||
0,
|
||||
0,
|
||||
10,
|
||||
10,
|
||||
func(result map[string]Dimensions) {
|
||||
assert.EqualValues(
|
||||
t,
|
||||
result,
|
||||
map[string]Dimensions{
|
||||
"static": {X0: 0, X1: 9, Y0: 0, Y1: 9},
|
||||
// not sure if X0: 10, X1: 9 makes any sense, but testing this in the
|
||||
// actual GUI it seems harmless
|
||||
"dynamic1": {X0: 10, X1: 9, Y0: 0, Y1: 9},
|
||||
"dynamic2": {X0: 10, X1: 9, Y0: 0, Y1: 9},
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
|
||||
@@ -6,6 +6,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/gui/presentation"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
@@ -86,16 +87,18 @@ func (gui *Gui) handleBranchPress() error {
|
||||
return gui.createErrorPanel(gui.Tr.AlreadyCheckedOutBranch)
|
||||
}
|
||||
branch := gui.getSelectedBranch()
|
||||
return gui.handleCheckoutRef(branch.Name, handleCheckoutRefOptions{})
|
||||
return gui.handleCheckoutRef(branch.Name, handleCheckoutRefOptions{span: gui.Tr.Spans.CheckoutBranch})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreatePullRequestPress() error {
|
||||
pullRequest := commands.NewPullRequest(gui.GitCommand)
|
||||
|
||||
branch := gui.getSelectedBranch()
|
||||
if err := pullRequest.Create(branch); err != nil {
|
||||
url, err := pullRequest.Create(branch)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(fmt.Sprintf("Creating pull request at URL: %s", url), "Create pull request", false))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -104,9 +107,11 @@ func (gui *Gui) handleCopyPullRequestURLPress() error {
|
||||
pullRequest := commands.NewPullRequest(gui.GitCommand)
|
||||
|
||||
branch := gui.getSelectedBranch()
|
||||
if err := pullRequest.CopyURL(branch); err != nil {
|
||||
url, err := pullRequest.CopyURL(branch)
|
||||
if 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)
|
||||
|
||||
@@ -118,7 +123,7 @@ func (gui *Gui) handleGitFetch() error {
|
||||
return err
|
||||
}
|
||||
go utils.Safe(func() {
|
||||
err := gui.fetch(true)
|
||||
err := gui.fetch(true, "Fetch")
|
||||
gui.handleCredentialsPopup(err)
|
||||
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
})
|
||||
@@ -134,7 +139,7 @@ func (gui *Gui) handleForceCheckout() error {
|
||||
title: title,
|
||||
prompt: message,
|
||||
handleConfirm: func() error {
|
||||
if err := gui.GitCommand.Checkout(branch.Name, commands.CheckoutOptions{Force: true}); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.ForceCheckoutBranch).Checkout(branch.Name, commands.CheckoutOptions{Force: true}); err != nil {
|
||||
_ = gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
@@ -146,6 +151,7 @@ type handleCheckoutRefOptions struct {
|
||||
WaitingStatus string
|
||||
EnvVars []string
|
||||
onRefNotFound func(ref string) error
|
||||
span string
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions) error {
|
||||
@@ -163,8 +169,10 @@ func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions)
|
||||
gui.State.Panels.Commits.LimitCommits = true
|
||||
}
|
||||
|
||||
gitCommand := gui.GitCommand.WithSpan(options.span)
|
||||
|
||||
return gui.WithWaitingStatus(waitingStatus, func() error {
|
||||
if err := gui.GitCommand.Checkout(ref, cmdOptions); err != nil {
|
||||
if err := gitCommand.Checkout(ref, cmdOptions); err != nil {
|
||||
// note, this will only work for english-language git commands. If we force git to use english, and the error isn't this one, then the user will receive an english command they may not understand. I'm not sure what the best solution to this is. Running the command once in english and a second time in the native language is one option
|
||||
|
||||
if options.onRefNotFound != nil && strings.Contains(err.Error(), "did not match any file(s) known to git") {
|
||||
@@ -178,15 +186,15 @@ func (gui *Gui) handleCheckoutRef(ref string, options handleCheckoutRefOptions)
|
||||
title: gui.Tr.AutoStashTitle,
|
||||
prompt: gui.Tr.AutoStashPrompt,
|
||||
handleConfirm: func() error {
|
||||
if err := gui.GitCommand.StashSave(gui.Tr.StashPrefix + ref); err != nil {
|
||||
if err := gitCommand.StashSave(gui.Tr.StashPrefix + ref); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
if err := gui.GitCommand.Checkout(ref, cmdOptions); err != nil {
|
||||
if err := gitCommand.Checkout(ref, cmdOptions); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
onSuccess()
|
||||
if err := gui.GitCommand.StashDo(0, "pop"); err != nil {
|
||||
if err := gitCommand.StashDo(0, "pop"); err != nil {
|
||||
if err := gui.refreshSidePanels(refreshOptions{mode: BLOCK_UI}); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -213,6 +221,7 @@ func (gui *Gui) handleCheckoutByName() error {
|
||||
findSuggestionsFunc: gui.findBranchNameSuggestions,
|
||||
handleConfirm: func(response string) error {
|
||||
return gui.handleCheckoutRef(response, handleCheckoutRefOptions{
|
||||
span: "Checkout branch",
|
||||
onRefNotFound: func(ref string) error {
|
||||
|
||||
return gui.ask(askOpts{
|
||||
@@ -285,7 +294,7 @@ func (gui *Gui) deleteNamedBranch(selectedBranch *models.Branch, force bool) err
|
||||
title: title,
|
||||
prompt: message,
|
||||
handleConfirm: func() error {
|
||||
if err := gui.GitCommand.DeleteBranch(selectedBranch.Name, force); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.DeleteBranch).DeleteBranch(selectedBranch.Name, force); err != nil {
|
||||
errMessage := err.Error()
|
||||
if !force && strings.Contains(errMessage, "is not fully merged") {
|
||||
return gui.deleteNamedBranch(selectedBranch, true)
|
||||
@@ -321,7 +330,7 @@ func (gui *Gui) mergeBranchIntoCheckedOutBranch(branchName string) error {
|
||||
title: gui.Tr.MergingTitle,
|
||||
prompt: prompt,
|
||||
handleConfirm: func() error {
|
||||
err := gui.GitCommand.Merge(branchName, commands.MergeOpts{})
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.Merge).Merge(branchName, commands.MergeOpts{})
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
},
|
||||
})
|
||||
@@ -362,7 +371,7 @@ func (gui *Gui) handleRebaseOntoBranch(selectedBranchName string) error {
|
||||
title: gui.Tr.RebasingTitle,
|
||||
prompt: prompt,
|
||||
handleConfirm: func() error {
|
||||
err := gui.GitCommand.RebaseBranch(selectedBranchName)
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.RebaseBranch).RebaseBranch(selectedBranchName)
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
},
|
||||
})
|
||||
@@ -388,6 +397,8 @@ func (gui *Gui) handleFastForward() error {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
span := gui.Tr.Spans.FastForwardBranch
|
||||
|
||||
split := strings.Split(upstream, "/")
|
||||
remoteName := split[0]
|
||||
remoteBranchName := strings.Join(split[1:], "/")
|
||||
@@ -403,9 +414,9 @@ func (gui *Gui) handleFastForward() error {
|
||||
_ = gui.createLoaderPanel(message)
|
||||
|
||||
if gui.State.Panels.Branches.SelectedLineIdx == 0 {
|
||||
_ = gui.pullWithMode("ff-only", PullFilesOptions{})
|
||||
_ = gui.pullWithMode("ff-only", PullFilesOptions{span: span})
|
||||
} else {
|
||||
err := gui.GitCommand.FastForward(branch.Name, remoteName, remoteBranchName, gui.promptUserForCredential)
|
||||
err := gui.GitCommand.WithSpan(span).FastForward(branch.Name, remoteName, remoteBranchName, gui.promptUserForCredential)
|
||||
gui.handleCredentialsPopup(err)
|
||||
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{BRANCHES}})
|
||||
}
|
||||
@@ -433,7 +444,7 @@ func (gui *Gui) handleRenameBranch() error {
|
||||
title: gui.Tr.NewBranchNamePrompt + " " + branch.Name + ":",
|
||||
initialContent: branch.Name,
|
||||
handleConfirm: func(newBranchName string) error {
|
||||
if err := gui.GitCommand.RenameBranch(branch.Name, newBranchName); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.RenameBranch).RenameBranch(branch.Name, newBranchName); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
@@ -494,15 +505,15 @@ func (gui *Gui) handleNewBranchOffCurrentItem() error {
|
||||
|
||||
prefilledName := ""
|
||||
if context.GetKey() == REMOTE_BRANCHES_CONTEXT_KEY {
|
||||
// will set to the remote's existing name
|
||||
prefilledName = item.ID()
|
||||
// will set to the remote's branch name without the remote name
|
||||
prefilledName = strings.SplitAfterN(item.ID(), "/", 2)[1]
|
||||
}
|
||||
|
||||
return gui.prompt(promptOpts{
|
||||
title: message,
|
||||
initialContent: prefilledName,
|
||||
handleConfirm: func(response string) error {
|
||||
if err := gui.GitCommand.NewBranch(sanitizedBranchName(response), item.ID()); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.CreateBranch).NewBranch(sanitizedBranchName(response), item.ID()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ func (gui *Gui) HandlePasteCommits() error {
|
||||
prompt: gui.Tr.SureCherryPick,
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.CherryPickingStatus, func() error {
|
||||
err := gui.GitCommand.CherryPickCommits(gui.State.Modes.CherryPicking.CherryPickedCommits)
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.CherryPick).CherryPickCommits(gui.State.Modes.CherryPicking.CherryPickedCommits)
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
},
|
||||
|
||||
185
pkg/gui/command_log_panel.go
Normal file
185
pkg/gui/command_log_panel.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/constants"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func (gui *Gui) GetOnRunCommand() func(entry oscommands.CmdLogEntry) {
|
||||
// closing over this so that nobody else can modify it
|
||||
currentSpan := ""
|
||||
|
||||
return func(entry oscommands.CmdLogEntry) {
|
||||
if gui.Views.Extras == nil {
|
||||
return
|
||||
}
|
||||
|
||||
gui.Views.Extras.Autoscroll = true
|
||||
|
||||
if entry.GetSpan() != currentSpan {
|
||||
fmt.Fprint(gui.Views.Extras, "\n"+utils.ColoredString(entry.GetSpan(), color.FgYellow))
|
||||
currentSpan = entry.GetSpan()
|
||||
}
|
||||
|
||||
clrAttr := theme.DefaultTextColor
|
||||
if !entry.GetCommandLine() {
|
||||
clrAttr = color.FgMagenta
|
||||
}
|
||||
gui.CmdLog = append(gui.CmdLog, entry.GetCmdStr())
|
||||
indentedCmdStr := " " + strings.Replace(entry.GetCmdStr(), "\n", "\n ", -1)
|
||||
fmt.Fprint(gui.Views.Extras, "\n"+utils.ColoredString(indentedCmdStr, clrAttr))
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) printCommandLogHeader() {
|
||||
introStr := fmt.Sprintf(
|
||||
gui.Tr.CommandLogHeader,
|
||||
gui.getKeyDisplay(gui.Config.GetUserConfig().Keybinding.Universal.ExtrasMenu),
|
||||
)
|
||||
fmt.Fprintln(gui.Views.Extras, utils.ColoredString(introStr, color.FgCyan))
|
||||
|
||||
if gui.Config.GetUserConfig().Gui.ShowRandomTip {
|
||||
fmt.Fprintf(
|
||||
gui.Views.Extras,
|
||||
"%s: %s",
|
||||
utils.ColoredString(gui.Tr.RandomTip, color.FgYellow),
|
||||
utils.ColoredString(gui.getRandomTip(), color.FgGreen),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) getRandomTip() string {
|
||||
config := gui.Config.GetUserConfig().Keybinding
|
||||
|
||||
formattedKey := func(key string) string {
|
||||
return gui.getKeyDisplay(key)
|
||||
}
|
||||
|
||||
tips := []string{
|
||||
// keybindings and lazygit-specific advice
|
||||
fmt.Sprintf(
|
||||
"To force push, press '%s' and then if the push is rejected you will be asked if you want to force push",
|
||||
formattedKey(config.Universal.PushFiles),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"To filter commits by path, press '%s'",
|
||||
formattedKey(config.Universal.FilteringMenu),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"To start an interactive rebase, press '%s' on a commit. You can always abort the rebase by pressing '%s' and selecting 'abort'",
|
||||
formattedKey(config.Universal.Edit),
|
||||
formattedKey(config.Universal.CreateRebaseOptionsMenu),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"In flat file view, merge conflicts are sorted to the top. To switch to flat file view press '%s'",
|
||||
formattedKey(config.Files.ToggleTreeView),
|
||||
),
|
||||
"If you want to learn Go and can think of ways to improve lazygit, join the team! Click 'Ask Question' and express your interest",
|
||||
fmt.Sprintf(
|
||||
"If you press '%s'/'%s' you can undo/redo your changes. Be wary though, this only applies to branches/commits, so only do this if your worktree is clear.\nDocs: %s",
|
||||
formattedKey(config.Universal.Undo),
|
||||
formattedKey(config.Universal.Redo),
|
||||
constants.Links.Docs.Undoing,
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"to hard reset onto your current upstream branch, press '%s' in the files panel",
|
||||
formattedKey(config.Files.ViewResetOptions),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"To push a tag, navigate to the tag in the tags tab and press '%s'",
|
||||
formattedKey(config.Branches.PushTag),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"You can view the individual files of a stash entry by pressing '%s'",
|
||||
formattedKey(config.Universal.GoInto),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"You can diff two commits by pressing '%s' one one commit and then navigating to the other. You can then press '%s' to view the files of the diff",
|
||||
formattedKey(config.Universal.DiffingMenu),
|
||||
formattedKey(config.Universal.GoInto),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"press '%s' on a commit to drop it (delete it)",
|
||||
formattedKey(config.Universal.Remove),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"If you need to pull out the big guns to resolve merge conflicts, you can press '%s' in the files panel to open 'git mergetool'",
|
||||
formattedKey(config.Files.OpenMergeTool),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"To revert a commit, press '%s' on that commit",
|
||||
formattedKey(config.Commits.RevertCommit),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"To escape a mode, for example cherry-picking, patch-building, diffing, or filtering mode, you can just spam the '%s' button. Unless of course you have `quitOnTopLevelReturn` enabled in your config",
|
||||
formattedKey(config.Universal.Return),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"To search for a string in your panel, press '%s'",
|
||||
formattedKey(config.Universal.StartSearch),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"You can page through the items of a panel using '%s' and '%s'",
|
||||
formattedKey(config.Universal.PrevPage),
|
||||
formattedKey(config.Universal.NextPage),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"You can jump to the top/bottom of a panel using '%s' and '%s'",
|
||||
formattedKey(config.Universal.GotoTop),
|
||||
formattedKey(config.Universal.GotoBottom),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"To collapse/expand a directory, press '%s'",
|
||||
formattedKey(config.Universal.GoInto),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"You can append your staged changes to an older commit by pressing '%s' on that commit",
|
||||
formattedKey(config.Commits.AmendToCommit),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"You can amend the last commit with your new file changes by pressing '%s' in the files panel",
|
||||
formattedKey(config.Files.AmendLastCommit),
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"You can now navigate the side panels with '%s' and '%s'",
|
||||
formattedKey(config.Universal.NextBlockAlt2),
|
||||
formattedKey(config.Universal.PrevBlockAlt2),
|
||||
),
|
||||
|
||||
"You can use lazygit with a bare repo by passing the --git-dir and --work-tree arguments as you would for the git CLI",
|
||||
|
||||
// general advice
|
||||
"`git commit` is really just the programmer equivalent of saving your game. Always do it before embarking on an ambitious change!",
|
||||
"Try to separate commits that refactor code from commits that add new functionality: if they're squashed into the one commit, it can be hard to spot what's new.",
|
||||
"If you ever want to experiment, it's easy to create a new branch off your current one and go nuts, then delete it afterwards",
|
||||
"Always read through the diff of your changes before assigning somebody to review your code. Better for you to catch any silly mistakes than your colleagues!",
|
||||
"If something goes wrong, you can always checkout a commit from your reflog to return to an earlier state",
|
||||
"The stash is a good place to save snippets of code that you always find yourself adding when debugging.",
|
||||
|
||||
// links
|
||||
fmt.Sprintf(
|
||||
"If you want a git diff with syntax colouring, check out lazygit's integration with delta:\n%s",
|
||||
constants.Links.Docs.CustomPagers,
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"You can build your own custom menus and commands to run from within lazygit. For examples see:\n%s",
|
||||
constants.Links.Docs.CustomCommands,
|
||||
),
|
||||
fmt.Sprintf(
|
||||
"If you ever find a bug, do not hesistate to raise an issue on the repo:\n%s",
|
||||
constants.Links.Issues,
|
||||
),
|
||||
}
|
||||
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
randomIndex := rand.Intn(len(tips))
|
||||
return tips[randomIndex]
|
||||
}
|
||||
@@ -62,7 +62,7 @@ func (gui *Gui) handleCheckoutCommitFile() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := gui.GitCommand.CheckoutFile(gui.State.CommitFileManager.GetParent(), node.GetPath()); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.CheckoutFile).CheckoutFile(gui.State.CommitFileManager.GetParent(), node.GetPath()); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ func (gui *Gui) handleDiscardOldFileChange() error {
|
||||
prompt: gui.Tr.DiscardFileChangesPrompt,
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error {
|
||||
if err := gui.GitCommand.DiscardOldFileChanges(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, fileName); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.DiscardOldFileChange).DiscardOldFileChanges(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, fileName); err != nil {
|
||||
if err := gui.handleGenericMergeCommandResult(err); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,27 +1,14 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// runSyncOrAsyncCommand takes the output of a command that may have returned
|
||||
// either no error, an error, or a subprocess to execute, and if a subprocess
|
||||
// needs to be run, it runs it
|
||||
func (gui *Gui) runSyncOrAsyncCommand(sub *exec.Cmd, err error) (bool, error) {
|
||||
if err != nil {
|
||||
return false, gui.surfaceError(err)
|
||||
}
|
||||
if sub != nil {
|
||||
return false, gui.runSubprocessWithSuspense(sub)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitConfirm() error {
|
||||
message := gui.trimmedContent(gui.Views.CommitMessage)
|
||||
if message == "" {
|
||||
@@ -32,17 +19,14 @@ func (gui *Gui) handleCommitConfirm() error {
|
||||
if skipHookPrefix != "" && strings.HasPrefix(message, skipHookPrefix) {
|
||||
flags = "--no-verify"
|
||||
}
|
||||
ok, err := gui.runSyncOrAsyncCommand(gui.GitCommand.Commit(message, flags))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.clearEditorView(gui.Views.CommitMessage)
|
||||
_ = gui.returnFromContext()
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
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 {
|
||||
_ = gui.returnFromContext()
|
||||
gui.clearEditorView(gui.Views.CommitMessage)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCommitClose() error {
|
||||
@@ -53,9 +37,9 @@ func (gui *Gui) handleCommitMessageFocused() error {
|
||||
message := utils.ResolvePlaceholderString(
|
||||
gui.Tr.CommitMessageConfirm,
|
||||
map[string]string{
|
||||
"keyBindClose": "esc",
|
||||
"keyBindConfirm": "enter",
|
||||
"keyBindNewLine": "tab",
|
||||
"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),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"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/utils"
|
||||
)
|
||||
|
||||
@@ -169,7 +171,7 @@ func (gui *Gui) handleCommitSquashDown() error {
|
||||
prompt: gui.Tr.SureSquashThisCommit,
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.SquashingStatus, func() error {
|
||||
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "squash")
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.SquashCommitDown).InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "squash")
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
},
|
||||
@@ -198,7 +200,7 @@ func (gui *Gui) handleCommitFixup() error {
|
||||
prompt: gui.Tr.SureFixupThisCommit,
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.FixingStatus, func() error {
|
||||
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "fixup")
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.FixupCommit).InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "fixup")
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
},
|
||||
@@ -236,7 +238,7 @@ func (gui *Gui) handleRenameCommit() error {
|
||||
title: gui.Tr.LcRenameCommit,
|
||||
initialContent: message,
|
||||
handleConfirm: func(response string) error {
|
||||
if err := gui.GitCommand.RenameCommit(response); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.RewordCommit).RenameCommit(response); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
@@ -258,12 +260,12 @@ func (gui *Gui) handleRenameCommitEditor() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
subProcess, err := gui.GitCommand.RewordCommit(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx)
|
||||
subProcess, err := gui.GitCommand.WithSpan(gui.Tr.Spans.RewordCommit).RewordCommit(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
if subProcess != nil {
|
||||
return gui.runSubprocessWithSuspense(subProcess)
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(subProcess)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -286,6 +288,12 @@ func (gui *Gui) handleMidRebaseCommand(action string) (bool, error) {
|
||||
return true, gui.createErrorPanel(gui.Tr.LcRewordNotSupported)
|
||||
}
|
||||
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(
|
||||
fmt.Sprintf("Updating rebase action of commit %s to '%s'", selectedCommit.ShortSha(), action),
|
||||
"Update rebase TODO",
|
||||
false,
|
||||
))
|
||||
|
||||
if err := gui.GitCommand.EditRebaseTodo(gui.State.Panels.Commits.SelectedLineIdx, action); err != nil {
|
||||
return false, gui.surfaceError(err)
|
||||
}
|
||||
@@ -311,7 +319,7 @@ func (gui *Gui) handleCommitDelete() error {
|
||||
prompt: gui.Tr.DeleteCommitPrompt,
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.DeletingStatus, func() error {
|
||||
err := gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "drop")
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.DropCommit).InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "drop")
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
},
|
||||
@@ -323,12 +331,23 @@ func (gui *Gui) handleCommitMoveDown() error {
|
||||
return err
|
||||
}
|
||||
|
||||
span := gui.Tr.Spans.MoveCommitDown
|
||||
|
||||
index := gui.State.Panels.Commits.SelectedLineIdx
|
||||
selectedCommit := gui.State.Commits[index]
|
||||
if selectedCommit.Status == "rebasing" {
|
||||
if gui.State.Commits[index+1].Status != "rebasing" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// logging directly here because MoveTodoDown doesn't have enough information
|
||||
// to provide a useful log
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(
|
||||
fmt.Sprintf("Moving commit %s down", selectedCommit.ShortSha()),
|
||||
span,
|
||||
false,
|
||||
))
|
||||
|
||||
if err := gui.GitCommand.MoveTodoDown(index); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
@@ -337,7 +356,7 @@ func (gui *Gui) handleCommitMoveDown() error {
|
||||
}
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.MovingStatus, func() error {
|
||||
err := gui.GitCommand.MoveCommitDown(gui.State.Commits, index)
|
||||
err := gui.GitCommand.WithSpan(span).MoveCommitDown(gui.State.Commits, index)
|
||||
if err == nil {
|
||||
gui.State.Panels.Commits.SelectedLineIdx++
|
||||
}
|
||||
@@ -354,8 +373,19 @@ func (gui *Gui) handleCommitMoveUp() error {
|
||||
if index == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
span := gui.Tr.Spans.MoveCommitUp
|
||||
|
||||
selectedCommit := gui.State.Commits[index]
|
||||
if selectedCommit.Status == "rebasing" {
|
||||
// logging directly here because MoveTodoDown doesn't have enough information
|
||||
// to provide a useful log
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(
|
||||
fmt.Sprintf("Moving commit %s up", selectedCommit.ShortSha()),
|
||||
span,
|
||||
false,
|
||||
))
|
||||
|
||||
if err := gui.GitCommand.MoveTodoDown(index - 1); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
@@ -364,7 +394,7 @@ func (gui *Gui) handleCommitMoveUp() error {
|
||||
}
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.MovingStatus, func() error {
|
||||
err := gui.GitCommand.MoveCommitDown(gui.State.Commits, index-1)
|
||||
err := gui.GitCommand.WithSpan(span).MoveCommitDown(gui.State.Commits, index-1)
|
||||
if err == nil {
|
||||
gui.State.Panels.Commits.SelectedLineIdx--
|
||||
}
|
||||
@@ -386,7 +416,7 @@ func (gui *Gui) handleCommitEdit() error {
|
||||
}
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error {
|
||||
err = gui.GitCommand.InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "edit")
|
||||
err = gui.GitCommand.WithSpan(gui.Tr.Spans.EditCommit).InteractiveRebase(gui.State.Commits, gui.State.Panels.Commits.SelectedLineIdx, "edit")
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
}
|
||||
@@ -401,7 +431,7 @@ func (gui *Gui) handleCommitAmendTo() error {
|
||||
prompt: gui.Tr.AmendCommitPrompt,
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.AmendingStatus, func() error {
|
||||
err := gui.GitCommand.AmendTo(gui.State.Commits[gui.State.Panels.Commits.SelectedLineIdx].Sha)
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.AmendCommit).AmendTo(gui.State.Commits[gui.State.Panels.Commits.SelectedLineIdx].Sha)
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
},
|
||||
@@ -431,7 +461,7 @@ func (gui *Gui) handleCommitRevert() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.GitCommand.Revert(gui.State.Commits[gui.State.Panels.Commits.SelectedLineIdx].Sha); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.RevertCommit).Revert(gui.State.Commits[gui.State.Panels.Commits.SelectedLineIdx].Sha); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
gui.State.Panels.Commits.SelectedLineIdx++
|
||||
@@ -468,7 +498,7 @@ func (gui *Gui) handleCreateFixupCommit() error {
|
||||
title: gui.Tr.CreateFixupCommit,
|
||||
prompt: prompt,
|
||||
handleConfirm: func() error {
|
||||
if err := gui.GitCommand.CreateFixupCommit(commit.Sha); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.CreateFixupCommit).CreateFixupCommit(commit.Sha); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
@@ -499,7 +529,7 @@ func (gui *Gui) handleSquashAllAboveFixupCommits() error {
|
||||
prompt: prompt,
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.SquashingStatus, func() error {
|
||||
err := gui.GitCommand.SquashAllAboveFixupCommits(commit.Sha)
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.SquashAllAboveFixupCommits).SquashAllAboveFixupCommits(commit.Sha)
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
},
|
||||
@@ -522,7 +552,7 @@ func (gui *Gui) handleCreateLightweightTag(commitSha string) error {
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.TagNameTitle,
|
||||
handleConfirm: func(response string) error {
|
||||
if err := gui.GitCommand.CreateLightweightTag(response, commitSha); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.CreateLightweightTag).CreateLightweightTag(response, commitSha); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{COMMITS, TAGS}})
|
||||
@@ -540,7 +570,7 @@ func (gui *Gui) handleCheckoutCommit() error {
|
||||
title: gui.Tr.LcCheckoutCommit,
|
||||
prompt: gui.Tr.SureCheckoutThisCommit,
|
||||
handleConfirm: func() error {
|
||||
return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{})
|
||||
return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{span: gui.Tr.Spans.CheckoutCommit})
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -595,7 +625,7 @@ func (gui *Gui) handleCopySelectedCommitMessageToClipboard() error {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
if err := gui.OSCommand.CopyToClipboard(message); err != nil {
|
||||
if err := gui.OSCommand.WithSpan(gui.Tr.Spans.CopyCommitMessageToClipboard).CopyToClipboard(message); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ package gui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
@@ -216,18 +215,16 @@ func (gui *Gui) createPopupPanel(opts createPopupPanelOpts) error {
|
||||
}
|
||||
gui.Views.Confirmation.Editable = opts.editable
|
||||
gui.Views.Confirmation.Editor = gocui.EditorFunc(gui.defaultEditor)
|
||||
if opts.editable {
|
||||
go utils.Safe(func() {
|
||||
// TODO: remove this wait (right now if you remove it the EditGotoToEndOfLine method doesn't seem to work)
|
||||
time.Sleep(time.Millisecond)
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
gui.Views.Confirmation.EditGotoToEndOfLine()
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
gui.renderString(gui.Views.Confirmation, opts.prompt)
|
||||
if opts.editable {
|
||||
if err := gui.Views.Confirmation.SetEditorContent(opts.prompt); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := gui.renderStringSync(gui.Views.Confirmation, opts.prompt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return gui.setKeyBindings(opts)
|
||||
})
|
||||
|
||||
@@ -13,111 +13,9 @@ const (
|
||||
MAIN_CONTEXT
|
||||
TEMPORARY_POPUP
|
||||
PERSISTENT_POPUP
|
||||
EXTRAS_CONTEXT
|
||||
)
|
||||
|
||||
type ContextKey string
|
||||
|
||||
const (
|
||||
STATUS_CONTEXT_KEY ContextKey = "status"
|
||||
FILES_CONTEXT_KEY ContextKey = "files"
|
||||
LOCAL_BRANCHES_CONTEXT_KEY ContextKey = "localBranches"
|
||||
REMOTES_CONTEXT_KEY ContextKey = "remotes"
|
||||
REMOTE_BRANCHES_CONTEXT_KEY ContextKey = "remoteBranches"
|
||||
TAGS_CONTEXT_KEY ContextKey = "tags"
|
||||
BRANCH_COMMITS_CONTEXT_KEY ContextKey = "commits"
|
||||
REFLOG_COMMITS_CONTEXT_KEY ContextKey = "reflogCommits"
|
||||
SUB_COMMITS_CONTEXT_KEY ContextKey = "subCommits"
|
||||
COMMIT_FILES_CONTEXT_KEY ContextKey = "commitFiles"
|
||||
STASH_CONTEXT_KEY ContextKey = "stash"
|
||||
MAIN_NORMAL_CONTEXT_KEY ContextKey = "normal"
|
||||
MAIN_MERGING_CONTEXT_KEY ContextKey = "merging"
|
||||
MAIN_PATCH_BUILDING_CONTEXT_KEY ContextKey = "patchBuilding"
|
||||
MAIN_STAGING_CONTEXT_KEY ContextKey = "staging"
|
||||
MENU_CONTEXT_KEY ContextKey = "menu"
|
||||
CREDENTIALS_CONTEXT_KEY ContextKey = "credentials"
|
||||
CONFIRMATION_CONTEXT_KEY ContextKey = "confirmation"
|
||||
SEARCH_CONTEXT_KEY ContextKey = "search"
|
||||
COMMIT_MESSAGE_CONTEXT_KEY ContextKey = "commitMessage"
|
||||
SUBMODULES_CONTEXT_KEY ContextKey = "submodules"
|
||||
SUGGESTIONS_CONTEXT_KEY ContextKey = "suggestions"
|
||||
)
|
||||
|
||||
var allContextKeys = []ContextKey{
|
||||
STATUS_CONTEXT_KEY,
|
||||
FILES_CONTEXT_KEY,
|
||||
LOCAL_BRANCHES_CONTEXT_KEY,
|
||||
REMOTES_CONTEXT_KEY,
|
||||
REMOTE_BRANCHES_CONTEXT_KEY,
|
||||
TAGS_CONTEXT_KEY,
|
||||
BRANCH_COMMITS_CONTEXT_KEY,
|
||||
REFLOG_COMMITS_CONTEXT_KEY,
|
||||
SUB_COMMITS_CONTEXT_KEY,
|
||||
COMMIT_FILES_CONTEXT_KEY,
|
||||
STASH_CONTEXT_KEY,
|
||||
MAIN_NORMAL_CONTEXT_KEY,
|
||||
MAIN_MERGING_CONTEXT_KEY,
|
||||
MAIN_PATCH_BUILDING_CONTEXT_KEY,
|
||||
MAIN_STAGING_CONTEXT_KEY,
|
||||
MENU_CONTEXT_KEY,
|
||||
CREDENTIALS_CONTEXT_KEY,
|
||||
CONFIRMATION_CONTEXT_KEY,
|
||||
SEARCH_CONTEXT_KEY,
|
||||
COMMIT_MESSAGE_CONTEXT_KEY,
|
||||
SUBMODULES_CONTEXT_KEY,
|
||||
SUGGESTIONS_CONTEXT_KEY,
|
||||
}
|
||||
|
||||
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
|
||||
Normal Context
|
||||
Staging Context
|
||||
PatchBuilding Context
|
||||
Merging Context
|
||||
Credentials Context
|
||||
Confirmation Context
|
||||
CommitMessage Context
|
||||
Search Context
|
||||
}
|
||||
|
||||
func (gui *Gui) allContexts() []Context {
|
||||
return []Context{
|
||||
gui.State.Contexts.Status,
|
||||
gui.State.Contexts.Files,
|
||||
gui.State.Contexts.Submodules,
|
||||
gui.State.Contexts.Branches,
|
||||
gui.State.Contexts.Remotes,
|
||||
gui.State.Contexts.RemoteBranches,
|
||||
gui.State.Contexts.Tags,
|
||||
gui.State.Contexts.BranchCommits,
|
||||
gui.State.Contexts.CommitFiles,
|
||||
gui.State.Contexts.ReflogCommits,
|
||||
gui.State.Contexts.Stash,
|
||||
gui.State.Contexts.Menu,
|
||||
gui.State.Contexts.Confirmation,
|
||||
gui.State.Contexts.Credentials,
|
||||
gui.State.Contexts.CommitMessage,
|
||||
gui.State.Contexts.Normal,
|
||||
gui.State.Contexts.Staging,
|
||||
gui.State.Contexts.Merging,
|
||||
gui.State.Contexts.PatchBuilding,
|
||||
gui.State.Contexts.SubCommits,
|
||||
gui.State.Contexts.Suggestions,
|
||||
}
|
||||
}
|
||||
|
||||
type Context interface {
|
||||
HandleFocus() error
|
||||
HandleFocusLost() error
|
||||
@@ -134,170 +32,6 @@ type Context interface {
|
||||
GetOptionsMap() map[string]string
|
||||
}
|
||||
|
||||
type BasicContext struct {
|
||||
OnFocus func() error
|
||||
OnFocusLost func() error
|
||||
OnRender func() error
|
||||
OnGetOptionsMap func() map[string]string
|
||||
Kind ContextKind
|
||||
Key ContextKey
|
||||
ViewName string
|
||||
}
|
||||
|
||||
func (c BasicContext) GetOptionsMap() map[string]string {
|
||||
if c.OnGetOptionsMap != nil {
|
||||
return c.OnGetOptionsMap()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c BasicContext) SetWindowName(windowName string) {
|
||||
panic("can't set window name on basic context")
|
||||
}
|
||||
|
||||
func (c BasicContext) GetWindowName() string {
|
||||
// TODO: fix this up
|
||||
return c.GetViewName()
|
||||
}
|
||||
|
||||
func (c BasicContext) SetParentContext(Context) {
|
||||
panic("can't set parent context on basic context")
|
||||
}
|
||||
|
||||
func (c BasicContext) GetParentContext() (Context, bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (c BasicContext) HandleRender() error {
|
||||
if c.OnRender != nil {
|
||||
return c.OnRender()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c BasicContext) GetViewName() string {
|
||||
return c.ViewName
|
||||
}
|
||||
|
||||
func (c BasicContext) HandleFocus() error {
|
||||
return c.OnFocus()
|
||||
}
|
||||
|
||||
func (c BasicContext) HandleFocusLost() error {
|
||||
if c.OnFocusLost != nil {
|
||||
return c.OnFocusLost()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c BasicContext) GetKind() ContextKind {
|
||||
return c.Kind
|
||||
}
|
||||
|
||||
func (c BasicContext) GetKey() ContextKey {
|
||||
return c.Key
|
||||
}
|
||||
|
||||
func (gui *Gui) contextTree() ContextTree {
|
||||
return ContextTree{
|
||||
Status: BasicContext{
|
||||
OnFocus: gui.handleStatusSelect,
|
||||
Kind: SIDE_CONTEXT,
|
||||
ViewName: "status",
|
||||
Key: STATUS_CONTEXT_KEY,
|
||||
},
|
||||
Files: gui.filesListContext(),
|
||||
Submodules: gui.submodulesListContext(),
|
||||
Menu: gui.menuListContext(),
|
||||
Remotes: gui.remotesListContext(),
|
||||
RemoteBranches: gui.remoteBranchesListContext(),
|
||||
BranchCommits: gui.branchCommitsListContext(),
|
||||
CommitFiles: gui.commitFilesListContext(),
|
||||
ReflogCommits: gui.reflogCommitsListContext(),
|
||||
SubCommits: gui.subCommitsListContext(),
|
||||
Branches: gui.branchesListContext(),
|
||||
Tags: gui.tagsListContext(),
|
||||
Stash: gui.stashListContext(),
|
||||
Normal: BasicContext{
|
||||
OnFocus: func() error {
|
||||
return nil // TODO: should we do something here? We should allow for scrolling the panel
|
||||
},
|
||||
Kind: MAIN_CONTEXT,
|
||||
ViewName: "main",
|
||||
Key: MAIN_NORMAL_CONTEXT_KEY,
|
||||
},
|
||||
Staging: BasicContext{
|
||||
OnFocus: func() error {
|
||||
return nil
|
||||
// TODO: centralise the code here
|
||||
// return gui.refreshStagingPanel(false, -1)
|
||||
},
|
||||
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)
|
||||
},
|
||||
Kind: MAIN_CONTEXT,
|
||||
ViewName: "main",
|
||||
Key: MAIN_PATCH_BUILDING_CONTEXT_KEY,
|
||||
},
|
||||
Merging: BasicContext{
|
||||
OnFocus: gui.refreshMergePanelWithLock,
|
||||
Kind: MAIN_CONTEXT,
|
||||
ViewName: "main",
|
||||
Key: MAIN_MERGING_CONTEXT_KEY,
|
||||
OnGetOptionsMap: gui.getMergingOptions,
|
||||
},
|
||||
Credentials: BasicContext{
|
||||
OnFocus: 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,
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (tree ContextTree) initialViewContextMap() map[string]Context {
|
||||
return map[string]Context{
|
||||
"status": tree.Status,
|
||||
"files": tree.Files,
|
||||
"branches": tree.Branches,
|
||||
"commits": tree.BranchCommits,
|
||||
"commitFiles": tree.CommitFiles,
|
||||
"stash": tree.Stash,
|
||||
"menu": tree.Menu,
|
||||
"confirmation": tree.Confirmation,
|
||||
"credentials": tree.Credentials,
|
||||
"commitMessage": tree.CommitMessage,
|
||||
"main": tree.Normal,
|
||||
"secondary": tree.Normal,
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) popupViewNames() []string {
|
||||
result := []string{}
|
||||
for _, context := range gui.allContexts() {
|
||||
@@ -309,52 +43,6 @@ func (gui *Gui) popupViewNames() []string {
|
||||
return result
|
||||
}
|
||||
|
||||
func (tree ContextTree) initialViewTabContextMap() map[string][]tabContext {
|
||||
return map[string][]tabContext{
|
||||
"branches": {
|
||||
{
|
||||
tab: "Local Branches",
|
||||
contexts: []Context{tree.Branches},
|
||||
},
|
||||
{
|
||||
tab: "Remotes",
|
||||
contexts: []Context{
|
||||
tree.Remotes,
|
||||
tree.RemoteBranches,
|
||||
},
|
||||
},
|
||||
{
|
||||
tab: "Tags",
|
||||
contexts: []Context{tree.Tags},
|
||||
},
|
||||
},
|
||||
"commits": {
|
||||
{
|
||||
tab: "Commits",
|
||||
contexts: []Context{tree.BranchCommits},
|
||||
},
|
||||
{
|
||||
tab: "Reflog",
|
||||
contexts: []Context{
|
||||
tree.ReflogCommits,
|
||||
},
|
||||
},
|
||||
},
|
||||
"files": {
|
||||
{
|
||||
tab: "Files",
|
||||
contexts: []Context{tree.Files},
|
||||
},
|
||||
{
|
||||
tab: "Submodules",
|
||||
contexts: []Context{
|
||||
tree.Submodules,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) currentContextKeyIgnoringPopups() ContextKey {
|
||||
gui.State.ContextManager.RLock()
|
||||
defer gui.State.ContextManager.RUnlock()
|
||||
@@ -416,7 +104,7 @@ func (gui *Gui) pushContextDirect(c Context) error {
|
||||
}
|
||||
}
|
||||
gui.State.ContextManager.ContextStack = []Context{c}
|
||||
} else if len(gui.State.ContextManager.ContextStack) == 0 || gui.State.ContextManager.ContextStack[len(gui.State.ContextManager.ContextStack)-1].GetKey() != c.GetKey() {
|
||||
} else if len(gui.State.ContextManager.ContextStack) == 0 || gui.currentContextWithoutLock().GetKey() != c.GetKey() {
|
||||
// Do not append if the one at the end is the same context (e.g. opening a menu from a menu)
|
||||
// In that case we'll just close the menu entirely when the user hits escape.
|
||||
|
||||
@@ -580,6 +268,10 @@ func (gui *Gui) currentContext() Context {
|
||||
gui.State.ContextManager.RLock()
|
||||
defer gui.State.ContextManager.RUnlock()
|
||||
|
||||
return gui.currentContextWithoutLock()
|
||||
}
|
||||
|
||||
func (gui *Gui) currentContextWithoutLock() Context {
|
||||
if len(gui.State.ContextManager.ContextStack) == 0 {
|
||||
return gui.defaultSideContext()
|
||||
}
|
||||
@@ -621,6 +313,29 @@ func (gui *Gui) currentSideContext() Context {
|
||||
return gui.defaultSideContext()
|
||||
}
|
||||
|
||||
// static as opposed to popup
|
||||
func (gui *Gui) currentStaticContext() Context {
|
||||
gui.State.ContextManager.RLock()
|
||||
defer gui.State.ContextManager.RUnlock()
|
||||
|
||||
stack := gui.State.ContextManager.ContextStack
|
||||
|
||||
if len(stack) == 0 {
|
||||
return gui.defaultSideContext()
|
||||
}
|
||||
|
||||
// find the first context in the stack without a popup type
|
||||
for i := range stack {
|
||||
context := stack[len(stack)-1-i]
|
||||
|
||||
if context.GetKind() != TEMPORARY_POPUP && context.GetKind() != PERSISTENT_POPUP {
|
||||
return context
|
||||
}
|
||||
}
|
||||
|
||||
return gui.defaultSideContext()
|
||||
}
|
||||
|
||||
func (gui *Gui) defaultSideContext() Context {
|
||||
if gui.State.Modes.Filtering.Active() {
|
||||
return gui.State.Contexts.BranchCommits
|
||||
@@ -670,7 +385,7 @@ func (gui *Gui) onViewFocusChange() error {
|
||||
|
||||
currentView := gui.g.CurrentView()
|
||||
for _, view := range gui.g.Views() {
|
||||
view.Highlight = view.Name() != "main" && view == currentView
|
||||
view.Highlight = view.Name() != "main" && view.Name() != "extras" && view == currentView
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
266
pkg/gui/context_config.go
Normal file
266
pkg/gui/context_config.go
Normal file
@@ -0,0 +1,266 @@
|
||||
package gui
|
||||
|
||||
type ContextKey string
|
||||
|
||||
const (
|
||||
STATUS_CONTEXT_KEY ContextKey = "status"
|
||||
FILES_CONTEXT_KEY ContextKey = "files"
|
||||
LOCAL_BRANCHES_CONTEXT_KEY ContextKey = "localBranches"
|
||||
REMOTES_CONTEXT_KEY ContextKey = "remotes"
|
||||
REMOTE_BRANCHES_CONTEXT_KEY ContextKey = "remoteBranches"
|
||||
TAGS_CONTEXT_KEY ContextKey = "tags"
|
||||
BRANCH_COMMITS_CONTEXT_KEY ContextKey = "commits"
|
||||
REFLOG_COMMITS_CONTEXT_KEY ContextKey = "reflogCommits"
|
||||
SUB_COMMITS_CONTEXT_KEY ContextKey = "subCommits"
|
||||
COMMIT_FILES_CONTEXT_KEY ContextKey = "commitFiles"
|
||||
STASH_CONTEXT_KEY ContextKey = "stash"
|
||||
MAIN_NORMAL_CONTEXT_KEY ContextKey = "normal"
|
||||
MAIN_MERGING_CONTEXT_KEY ContextKey = "merging"
|
||||
MAIN_PATCH_BUILDING_CONTEXT_KEY ContextKey = "patchBuilding"
|
||||
MAIN_STAGING_CONTEXT_KEY ContextKey = "staging"
|
||||
MENU_CONTEXT_KEY ContextKey = "menu"
|
||||
CREDENTIALS_CONTEXT_KEY ContextKey = "credentials"
|
||||
CONFIRMATION_CONTEXT_KEY ContextKey = "confirmation"
|
||||
SEARCH_CONTEXT_KEY ContextKey = "search"
|
||||
COMMIT_MESSAGE_CONTEXT_KEY ContextKey = "commitMessage"
|
||||
SUBMODULES_CONTEXT_KEY ContextKey = "submodules"
|
||||
SUGGESTIONS_CONTEXT_KEY ContextKey = "suggestions"
|
||||
COMMAND_LOG_CONTEXT_KEY ContextKey = "cmdLog"
|
||||
)
|
||||
|
||||
var allContextKeys = []ContextKey{
|
||||
STATUS_CONTEXT_KEY,
|
||||
FILES_CONTEXT_KEY,
|
||||
LOCAL_BRANCHES_CONTEXT_KEY,
|
||||
REMOTES_CONTEXT_KEY,
|
||||
REMOTE_BRANCHES_CONTEXT_KEY,
|
||||
TAGS_CONTEXT_KEY,
|
||||
BRANCH_COMMITS_CONTEXT_KEY,
|
||||
REFLOG_COMMITS_CONTEXT_KEY,
|
||||
SUB_COMMITS_CONTEXT_KEY,
|
||||
COMMIT_FILES_CONTEXT_KEY,
|
||||
STASH_CONTEXT_KEY,
|
||||
MAIN_NORMAL_CONTEXT_KEY,
|
||||
MAIN_MERGING_CONTEXT_KEY,
|
||||
MAIN_PATCH_BUILDING_CONTEXT_KEY,
|
||||
MAIN_STAGING_CONTEXT_KEY,
|
||||
MENU_CONTEXT_KEY,
|
||||
CREDENTIALS_CONTEXT_KEY,
|
||||
CONFIRMATION_CONTEXT_KEY,
|
||||
SEARCH_CONTEXT_KEY,
|
||||
COMMIT_MESSAGE_CONTEXT_KEY,
|
||||
SUBMODULES_CONTEXT_KEY,
|
||||
SUGGESTIONS_CONTEXT_KEY,
|
||||
COMMAND_LOG_CONTEXT_KEY,
|
||||
}
|
||||
|
||||
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
|
||||
Normal Context
|
||||
Staging Context
|
||||
PatchBuilding Context
|
||||
Merging Context
|
||||
Credentials Context
|
||||
Confirmation Context
|
||||
CommitMessage Context
|
||||
Search Context
|
||||
CommandLog Context
|
||||
}
|
||||
|
||||
func (gui *Gui) allContexts() []Context {
|
||||
return []Context{
|
||||
gui.State.Contexts.Status,
|
||||
gui.State.Contexts.Files,
|
||||
gui.State.Contexts.Submodules,
|
||||
gui.State.Contexts.Branches,
|
||||
gui.State.Contexts.Remotes,
|
||||
gui.State.Contexts.RemoteBranches,
|
||||
gui.State.Contexts.Tags,
|
||||
gui.State.Contexts.BranchCommits,
|
||||
gui.State.Contexts.CommitFiles,
|
||||
gui.State.Contexts.ReflogCommits,
|
||||
gui.State.Contexts.Stash,
|
||||
gui.State.Contexts.Menu,
|
||||
gui.State.Contexts.Confirmation,
|
||||
gui.State.Contexts.Credentials,
|
||||
gui.State.Contexts.CommitMessage,
|
||||
gui.State.Contexts.Normal,
|
||||
gui.State.Contexts.Staging,
|
||||
gui.State.Contexts.Merging,
|
||||
gui.State.Contexts.PatchBuilding,
|
||||
gui.State.Contexts.SubCommits,
|
||||
gui.State.Contexts.Suggestions,
|
||||
gui.State.Contexts.CommandLog,
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) contextTree() ContextTree {
|
||||
return ContextTree{
|
||||
Status: &BasicContext{
|
||||
OnFocus: gui.handleStatusSelect,
|
||||
Kind: SIDE_CONTEXT,
|
||||
ViewName: "status",
|
||||
Key: STATUS_CONTEXT_KEY,
|
||||
},
|
||||
Files: gui.filesListContext(),
|
||||
Submodules: gui.submodulesListContext(),
|
||||
Menu: gui.menuListContext(),
|
||||
Remotes: gui.remotesListContext(),
|
||||
RemoteBranches: gui.remoteBranchesListContext(),
|
||||
BranchCommits: gui.branchCommitsListContext(),
|
||||
CommitFiles: gui.commitFilesListContext(),
|
||||
ReflogCommits: gui.reflogCommitsListContext(),
|
||||
SubCommits: gui.subCommitsListContext(),
|
||||
Branches: gui.branchesListContext(),
|
||||
Tags: gui.tagsListContext(),
|
||||
Stash: gui.stashListContext(),
|
||||
Normal: &BasicContext{
|
||||
OnFocus: func() error {
|
||||
return nil // TODO: should we do something here? We should allow for scrolling the panel
|
||||
},
|
||||
Kind: MAIN_CONTEXT,
|
||||
ViewName: "main",
|
||||
Key: MAIN_NORMAL_CONTEXT_KEY,
|
||||
},
|
||||
Staging: &BasicContext{
|
||||
OnFocus: func() error {
|
||||
return nil
|
||||
// TODO: centralise the code here
|
||||
// return gui.refreshStagingPanel(false, -1)
|
||||
},
|
||||
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)
|
||||
},
|
||||
Kind: MAIN_CONTEXT,
|
||||
ViewName: "main",
|
||||
Key: MAIN_PATCH_BUILDING_CONTEXT_KEY,
|
||||
},
|
||||
Merging: &BasicContext{
|
||||
OnFocus: gui.refreshMergePanelWithLock,
|
||||
Kind: MAIN_CONTEXT,
|
||||
ViewName: "main",
|
||||
Key: MAIN_MERGING_CONTEXT_KEY,
|
||||
OnGetOptionsMap: gui.getMergingOptions,
|
||||
},
|
||||
Credentials: &BasicContext{
|
||||
OnFocus: 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,
|
||||
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,
|
||||
OnGetOptionsMap: gui.getMergingOptions,
|
||||
OnFocusLost: func() error {
|
||||
gui.Views.Extras.Autoscroll = true
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (tree ContextTree) initialViewContextMap() map[string]Context {
|
||||
return map[string]Context{
|
||||
"status": tree.Status,
|
||||
"files": tree.Files,
|
||||
"branches": tree.Branches,
|
||||
"commits": tree.BranchCommits,
|
||||
"commitFiles": tree.CommitFiles,
|
||||
"stash": tree.Stash,
|
||||
"menu": tree.Menu,
|
||||
"confirmation": tree.Confirmation,
|
||||
"credentials": tree.Credentials,
|
||||
"commitMessage": tree.CommitMessage,
|
||||
"main": tree.Normal,
|
||||
"secondary": tree.Normal,
|
||||
"extras": tree.CommandLog,
|
||||
}
|
||||
}
|
||||
|
||||
func (tree ContextTree) initialViewTabContextMap() map[string][]tabContext {
|
||||
return map[string][]tabContext{
|
||||
"branches": {
|
||||
{
|
||||
tab: "Local Branches",
|
||||
contexts: []Context{tree.Branches},
|
||||
},
|
||||
{
|
||||
tab: "Remotes",
|
||||
contexts: []Context{
|
||||
tree.Remotes,
|
||||
tree.RemoteBranches,
|
||||
},
|
||||
},
|
||||
{
|
||||
tab: "Tags",
|
||||
contexts: []Context{tree.Tags},
|
||||
},
|
||||
},
|
||||
"commits": {
|
||||
{
|
||||
tab: "Commits",
|
||||
contexts: []Context{tree.BranchCommits},
|
||||
},
|
||||
{
|
||||
tab: "Reflog",
|
||||
contexts: []Context{
|
||||
tree.ReflogCommits,
|
||||
},
|
||||
},
|
||||
},
|
||||
"files": {
|
||||
{
|
||||
tab: "Files",
|
||||
contexts: []Context{tree.Files},
|
||||
},
|
||||
{
|
||||
tab: "Submodules",
|
||||
contexts: []Context{
|
||||
tree.Submodules,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ func (gui *Gui) handleSubmitCredential() error {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{})
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCloseCredentialsView() error {
|
||||
@@ -78,6 +78,7 @@ func (gui *Gui) handleCredentialsPopup(cmdErr error) {
|
||||
if strings.Contains(errMessage, "Invalid username, password or passphrase") {
|
||||
errMessage = gui.Tr.PassUnameWrong
|
||||
}
|
||||
_ = gui.returnFromContext()
|
||||
// we are not logging this error because it may contain a password or a passphrase
|
||||
_ = gui.createErrorPanel(errMessage)
|
||||
} else {
|
||||
|
||||
@@ -60,7 +60,7 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
|
||||
}
|
||||
|
||||
if customCommand.Subprocess {
|
||||
return gui.runSubprocessWithSuspense(gui.OSCommand.PrepareShellSubProcess(cmdStr))
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(gui.OSCommand.PrepareShellSubProcess(cmdStr))
|
||||
}
|
||||
|
||||
loadingText := customCommand.LoadingText
|
||||
@@ -68,7 +68,7 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
|
||||
loadingText = gui.Tr.LcRunningCustomCommandStatus
|
||||
}
|
||||
return gui.WithWaitingStatus(loadingText, func() error {
|
||||
if err := gui.OSCommand.RunShellCommand(cmdStr); err != nil {
|
||||
if err := gui.OSCommand.WithSpan(gui.Tr.Spans.CustomCommand).RunShellCommand(cmdStr); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{})
|
||||
|
||||
@@ -12,7 +12,7 @@ func (gui *Gui) handleCreateDiscardMenu() error {
|
||||
{
|
||||
displayString: gui.Tr.LcDiscardAllChanges,
|
||||
onPress: func() error {
|
||||
if err := gui.GitCommand.DiscardAllDirChanges(node); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.DiscardAllChangesInDirectory).DiscardAllDirChanges(node); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}})
|
||||
@@ -24,7 +24,7 @@ func (gui *Gui) handleCreateDiscardMenu() error {
|
||||
menuItems = append(menuItems, &menuItem{
|
||||
displayString: gui.Tr.LcDiscardUnstagedChanges,
|
||||
onPress: func() error {
|
||||
if err := gui.GitCommand.DiscardUnstagedDirChanges(node); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.DiscardUnstagedChangesInDirectory).DiscardUnstagedDirChanges(node); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func (gui *Gui) handleCreateDiscardMenu() error {
|
||||
{
|
||||
displayString: gui.Tr.LcDiscardAllChanges,
|
||||
onPress: func() error {
|
||||
if err := gui.GitCommand.DiscardAllFileChanges(file); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.DiscardAllChangesInFile).DiscardAllFileChanges(file); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}})
|
||||
@@ -64,7 +64,7 @@ func (gui *Gui) handleCreateDiscardMenu() error {
|
||||
menuItems = append(menuItems, &menuItem{
|
||||
displayString: gui.Tr.LcDiscardUnstagedChanges,
|
||||
onPress: func() error {
|
||||
if err := gui.GitCommand.DiscardUnstagedFileChanges(file); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.DiscardAllUnstagedChangesInFile).DiscardUnstagedFileChanges(file); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
|
||||
49
pkg/gui/extras_panel.go
Normal file
49
pkg/gui/extras_panel.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package gui
|
||||
|
||||
func (gui *Gui) handleCreateExtrasMenuPanel() error {
|
||||
menuItems := []*menuItem{
|
||||
{
|
||||
displayString: gui.Tr.ToggleShowCommandLog,
|
||||
onPress: func() error {
|
||||
currentContext := gui.currentStaticContext()
|
||||
if gui.ShowExtrasWindow && currentContext.GetKey() == COMMAND_LOG_CONTEXT_KEY {
|
||||
if err := gui.returnFromContext(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
gui.ShowExtrasWindow = !gui.ShowExtrasWindow
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: gui.Tr.FocusCommandLog,
|
||||
onPress: func() error {
|
||||
return gui.handleFocusCommandLog()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return gui.createMenu(gui.Tr.CommandLog, menuItems, createMenuOptions{showCancel: true})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleFocusCommandLog() error {
|
||||
gui.ShowExtrasWindow = true
|
||||
gui.State.Contexts.CommandLog.SetParentContext(gui.currentSideContext())
|
||||
return gui.pushContext(gui.State.Contexts.CommandLog)
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollUpExtra() error {
|
||||
gui.Views.Extras.Autoscroll = false
|
||||
|
||||
return gui.scrollUpView(gui.Views.Extras)
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollDownExtra() error {
|
||||
gui.Views.Extras.Autoscroll = false
|
||||
|
||||
if err := gui.scrollDownView(gui.Views.Extras); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"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/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
@@ -218,11 +219,11 @@ func (gui *Gui) handleFilePress() error {
|
||||
}
|
||||
|
||||
if file.HasUnstagedChanges {
|
||||
if err := gui.GitCommand.StageFile(file.Name); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.StageFile).StageFile(file.Name); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
} else {
|
||||
if err := gui.GitCommand.UnStageFile(file.Names(), file.Tracked); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.UnstageFile).UnStageFile(file.Names(), file.Tracked); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
}
|
||||
@@ -234,12 +235,12 @@ func (gui *Gui) handleFilePress() error {
|
||||
}
|
||||
|
||||
if node.GetHasUnstagedChanges() {
|
||||
if err := gui.GitCommand.StageFile(node.Path); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.StageFile).StageFile(node.Path); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
} else {
|
||||
// pretty sure it doesn't matter that we're always passing true here
|
||||
if err := gui.GitCommand.UnStageFile([]string{node.Path}, true); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.UnstageFile).UnStageFile([]string{node.Path}, true); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
}
|
||||
@@ -268,9 +269,9 @@ func (gui *Gui) focusAndSelectFile() error {
|
||||
func (gui *Gui) handleStageAll() error {
|
||||
var err error
|
||||
if gui.allFilesStaged() {
|
||||
err = gui.GitCommand.UnstageAll()
|
||||
err = gui.GitCommand.WithSpan(gui.Tr.Spans.UnstageAllFiles).UnstageAll()
|
||||
} else {
|
||||
err = gui.GitCommand.StageAll()
|
||||
err = gui.GitCommand.WithSpan(gui.Tr.Spans.StageAllFiles).StageAll()
|
||||
}
|
||||
if err != nil {
|
||||
_ = gui.surfaceError(err)
|
||||
@@ -293,10 +294,12 @@ func (gui *Gui) handleIgnoreFile() error {
|
||||
return gui.createErrorPanel("Cannot ignore .gitignore")
|
||||
}
|
||||
|
||||
gitCommand := gui.GitCommand.WithSpan(gui.Tr.Spans.IgnoreFile)
|
||||
|
||||
unstageFiles := func() error {
|
||||
return node.ForEachFile(func(file *models.File) error {
|
||||
if file.HasStagedChanges {
|
||||
if err := gui.GitCommand.UnStageFile(file.Names(), file.Tracked); err != nil {
|
||||
if err := gitCommand.UnStageFile(file.Names(), file.Tracked); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -315,11 +318,11 @@ func (gui *Gui) handleIgnoreFile() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.GitCommand.RemoveTrackedFiles(node.GetPath()); err != nil {
|
||||
if err := gitCommand.RemoveTrackedFiles(node.GetPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.GitCommand.Ignore(node.GetPath()); err != nil {
|
||||
if err := gitCommand.Ignore(node.GetPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}})
|
||||
@@ -331,7 +334,7 @@ func (gui *Gui) handleIgnoreFile() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.GitCommand.Ignore(node.GetPath()); err != nil {
|
||||
if err := gitCommand.Ignore(node.GetPath()); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
@@ -339,13 +342,12 @@ func (gui *Gui) handleIgnoreFile() error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleWIPCommitPress() error {
|
||||
skipHookPreifx := gui.Config.GetUserConfig().Git.SkipHookPrefix
|
||||
if skipHookPreifx == "" {
|
||||
skipHookPrefix := gui.Config.GetUserConfig().Git.SkipHookPrefix
|
||||
if skipHookPrefix == "" {
|
||||
return gui.createErrorPanel(gui.Tr.SkipHookPrefixNotConfigured)
|
||||
}
|
||||
|
||||
_ = gui.renderStringSync(gui.Views.CommitMessage, skipHookPreifx)
|
||||
if err := gui.Views.CommitMessage.SetCursor(len(skipHookPreifx), 0); err != nil {
|
||||
if err := gui.Views.CommitMessage.SetEditorContent(skipHookPrefix); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -364,7 +366,7 @@ func (gui *Gui) commitPrefixConfigForRepo() *config.CommitPrefixConfig {
|
||||
func (gui *Gui) prepareFilesForCommit() error {
|
||||
noStagedFiles := len(gui.stagedFiles()) == 0
|
||||
if noStagedFiles && gui.Config.GetUserConfig().Gui.SkipNoStagedFilesWarning {
|
||||
err := gui.GitCommand.StageAll()
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.StageAllFiles).StageAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -419,7 +421,7 @@ func (gui *Gui) promptToStageAllAndRetry(retry func() error) error {
|
||||
title: gui.Tr.NoFilesStagedTitle,
|
||||
prompt: gui.Tr.NoFilesStagedPrompt,
|
||||
handleConfirm: func() error {
|
||||
if err := gui.GitCommand.StageAll(); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.StageAllFiles).StageAll(); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
if err := gui.refreshFilesAndSubmodules(); err != nil {
|
||||
@@ -448,17 +450,9 @@ func (gui *Gui) handleAmendCommitPress() error {
|
||||
title: strings.Title(gui.Tr.AmendLastCommit),
|
||||
prompt: gui.Tr.SureToAmend,
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.AmendingStatus, func() error {
|
||||
ok, err := gui.runSyncOrAsyncCommand(gui.GitCommand.AmendHead())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
})
|
||||
cmdStr := gui.GitCommand.AmendHeadCmdStr()
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(cmdStr, gui.Tr.Spans.AmendCommit, true))
|
||||
return gui.withGpgHandling(cmdStr, gui.Tr.AmendingStatus, nil)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -474,14 +468,20 @@ func (gui *Gui) handleCommitEditorPress() error {
|
||||
return gui.promptToStageAllAndRetry(gui.handleCommitEditorPress)
|
||||
}
|
||||
|
||||
return gui.runSubprocessWithSuspense(
|
||||
gui.OSCommand.PrepareSubProcess("git", "commit"),
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(
|
||||
gui.OSCommand.WithSpan(gui.Tr.Spans.Commit).PrepareSubProcess("git", "commit"),
|
||||
)
|
||||
}
|
||||
|
||||
func (gui *Gui) editFile(filename string) error {
|
||||
_, err := gui.runSyncOrAsyncCommand(gui.GitCommand.EditFile(filename))
|
||||
return err
|
||||
cmdStr, err := gui.GitCommand.EditFileCmdStr(filename)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(
|
||||
gui.OSCommand.WithSpan(gui.Tr.Spans.EditFile).PrepareShellSubProcess(cmdStr),
|
||||
)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleFileEdit() error {
|
||||
@@ -608,6 +608,8 @@ func (gui *Gui) handlePullFiles() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
span := gui.Tr.Spans.Pull
|
||||
|
||||
currentBranch := gui.currentBranch()
|
||||
if currentBranch == nil {
|
||||
// need to wait for branches to refresh
|
||||
@@ -623,7 +625,7 @@ func (gui *Gui) handlePullFiles() error {
|
||||
}
|
||||
for branchName, branch := range conf.Branches {
|
||||
if branchName == currentBranch.Name {
|
||||
return gui.pullFiles(PullFilesOptions{RemoteName: branch.Remote, BranchName: branch.Name})
|
||||
return gui.pullFiles(PullFilesOptions{RemoteName: branch.Remote, BranchName: branch.Name, span: span})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -638,17 +640,18 @@ func (gui *Gui) handlePullFiles() error {
|
||||
}
|
||||
return gui.createErrorPanel(errorMessage)
|
||||
}
|
||||
return gui.pullFiles(PullFilesOptions{})
|
||||
return gui.pullFiles(PullFilesOptions{span: span})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return gui.pullFiles(PullFilesOptions{})
|
||||
return gui.pullFiles(PullFilesOptions{span: span})
|
||||
}
|
||||
|
||||
type PullFilesOptions struct {
|
||||
RemoteName string
|
||||
BranchName string
|
||||
span string
|
||||
}
|
||||
|
||||
func (gui *Gui) pullFiles(opts PullFilesOptions) error {
|
||||
@@ -658,6 +661,7 @@ func (gui *Gui) pullFiles(opts PullFilesOptions) error {
|
||||
|
||||
mode := gui.Config.GetUserConfig().Git.Pull.Mode
|
||||
|
||||
// TODO: this doesn't look like a good idea. Why the goroutine?
|
||||
go utils.Safe(func() { _ = gui.pullWithMode(mode, opts) })
|
||||
|
||||
return nil
|
||||
@@ -667,7 +671,9 @@ func (gui *Gui) pullWithMode(mode string, opts PullFilesOptions) error {
|
||||
gui.Mutexes.FetchMutex.Lock()
|
||||
defer gui.Mutexes.FetchMutex.Unlock()
|
||||
|
||||
err := gui.GitCommand.Fetch(
|
||||
gitCommand := gui.GitCommand.WithSpan(opts.span)
|
||||
|
||||
err := gitCommand.Fetch(
|
||||
commands.FetchOptions{
|
||||
PromptUserForCredential: gui.promptUserForCredential,
|
||||
RemoteName: opts.RemoteName,
|
||||
@@ -681,13 +687,13 @@ func (gui *Gui) pullWithMode(mode string, opts PullFilesOptions) error {
|
||||
|
||||
switch mode {
|
||||
case "rebase":
|
||||
err := gui.GitCommand.RebaseBranch("FETCH_HEAD")
|
||||
err := gitCommand.RebaseBranch("FETCH_HEAD")
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
case "merge":
|
||||
err := gui.GitCommand.Merge("FETCH_HEAD", commands.MergeOpts{})
|
||||
err := gitCommand.Merge("FETCH_HEAD", commands.MergeOpts{})
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
case "ff-only":
|
||||
err := gui.GitCommand.Merge("FETCH_HEAD", commands.MergeOpts{FastForwardOnly: true})
|
||||
err := gitCommand.Merge("FETCH_HEAD", commands.MergeOpts{FastForwardOnly: true})
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
default:
|
||||
return gui.createErrorPanel(fmt.Sprintf("git pull mode '%s' unrecognised", mode))
|
||||
@@ -700,7 +706,7 @@ func (gui *Gui) pushWithForceFlag(force bool, upstream string, args string) erro
|
||||
}
|
||||
go utils.Safe(func() {
|
||||
branchName := gui.getCheckedOutBranch().Name
|
||||
err := gui.GitCommand.Push(branchName, force, upstream, args, gui.promptUserForCredential)
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.Push).Push(branchName, force, upstream, args, gui.promptUserForCredential)
|
||||
if err != nil && !force && strings.Contains(err.Error(), "Updates were rejected") {
|
||||
forcePushDisabled := gui.Config.GetUserConfig().Git.DisableForcePushing
|
||||
if forcePushDisabled {
|
||||
@@ -729,6 +735,10 @@ func (gui *Gui) pushFiles() error {
|
||||
|
||||
// if we have pullables we'll ask if the user wants to force push
|
||||
currentBranch := gui.currentBranch()
|
||||
if currentBranch == nil {
|
||||
// need to wait for branches to refresh
|
||||
return nil
|
||||
}
|
||||
|
||||
if currentBranch.Pullables == "?" {
|
||||
// see if we have this branch in our config with an upstream
|
||||
@@ -785,7 +795,7 @@ func (gui *Gui) handleSwitchToMerge() error {
|
||||
}
|
||||
|
||||
func (gui *Gui) openFile(filename string) error {
|
||||
if err := gui.OSCommand.OpenFile(filename); err != nil {
|
||||
if err := gui.OSCommand.WithSpan(gui.Tr.Spans.OpenFile).OpenFile(filename); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return nil
|
||||
@@ -804,7 +814,8 @@ func (gui *Gui) handleCustomCommand() error {
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.CustomCommand,
|
||||
handleConfirm: func(command string) error {
|
||||
return gui.runSubprocessWithSuspense(
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(command, gui.Tr.Spans.CustomCommand, true))
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(
|
||||
gui.OSCommand.PrepareShellSubProcess(command),
|
||||
)
|
||||
},
|
||||
@@ -816,13 +827,13 @@ func (gui *Gui) handleCreateStashMenu() error {
|
||||
{
|
||||
displayString: gui.Tr.LcStashAllChanges,
|
||||
onPress: func() error {
|
||||
return gui.handleStashSave(gui.GitCommand.StashSave)
|
||||
return gui.handleStashSave(gui.GitCommand.WithSpan(gui.Tr.Spans.StashAllChanges).StashSave)
|
||||
},
|
||||
},
|
||||
{
|
||||
displayString: gui.Tr.LcStashStagedChanges,
|
||||
onPress: func() error {
|
||||
return gui.handleStashSave(gui.GitCommand.StashSaveStagedChanges)
|
||||
return gui.handleStashSave(gui.GitCommand.WithSpan(gui.Tr.Spans.StashStagedChanges).StashSaveStagedChanges)
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -879,3 +890,15 @@ func (gui *Gui) handleToggleFileTreeView() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleOpenMergeTool() error {
|
||||
return gui.ask(askOpts{
|
||||
title: gui.Tr.MergeToolTitle,
|
||||
prompt: gui.Tr.MergeToolPrompt,
|
||||
handleConfirm: func() error {
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(
|
||||
gui.OSCommand.ExecutableFromString(gui.GitCommand.OpenMergeToolCmd()),
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -14,18 +12,17 @@ func BuildTreeFromFiles(files []*models.File) *FileNode {
|
||||
|
||||
var curr *FileNode
|
||||
for _, file := range files {
|
||||
split := strings.Split(file.Name, string(os.PathSeparator))
|
||||
splitPath := split(file.Name)
|
||||
curr = root
|
||||
outer:
|
||||
for i := range split {
|
||||
for i := range splitPath {
|
||||
var setFile *models.File
|
||||
isFile := i == len(split)-1
|
||||
isFile := i == len(splitPath)-1
|
||||
if isFile {
|
||||
setFile = file
|
||||
}
|
||||
|
||||
path := filepath.Join(split[:i+1]...)
|
||||
|
||||
path := join(splitPath[:i+1])
|
||||
for _, existingChild := range curr.Children {
|
||||
if existingChild.Path == path {
|
||||
curr = existingChild
|
||||
@@ -61,17 +58,17 @@ func BuildTreeFromCommitFiles(files []*models.CommitFile) *CommitFileNode {
|
||||
|
||||
var curr *CommitFileNode
|
||||
for _, file := range files {
|
||||
split := strings.Split(file.Name, string(os.PathSeparator))
|
||||
splitPath := split(file.Name)
|
||||
curr = root
|
||||
outer:
|
||||
for i := range split {
|
||||
for i := range splitPath {
|
||||
var setFile *models.CommitFile
|
||||
isFile := i == len(split)-1
|
||||
isFile := i == len(splitPath)-1
|
||||
if isFile {
|
||||
setFile = file
|
||||
}
|
||||
|
||||
path := filepath.Join(split[:i+1]...)
|
||||
path := join(splitPath[:i+1])
|
||||
|
||||
for _, existingChild := range curr.Children {
|
||||
if existingChild.Path == path {
|
||||
@@ -100,11 +97,44 @@ func BuildFlatTreeFromFiles(files []*models.File) *FileNode {
|
||||
rootAux := BuildTreeFromFiles(files)
|
||||
sortedFiles := rootAux.GetLeaves()
|
||||
|
||||
// Move merge conflicts to top. This is the one way in which sorting
|
||||
// differs between flat mode and tree mode
|
||||
// from top down we have merge conflict files, then tracked file, then untracked
|
||||
// files. This is the one way in which sorting differs between flat mode and
|
||||
// tree mode
|
||||
sort.SliceStable(sortedFiles, func(i, j int) bool {
|
||||
return sortedFiles[i].File != nil && sortedFiles[i].File.HasMergeConflicts && !(sortedFiles[j].File != nil && sortedFiles[j].File.HasMergeConflicts)
|
||||
iFile := sortedFiles[i].File
|
||||
jFile := sortedFiles[j].File
|
||||
|
||||
// never going to happen but just to be safe
|
||||
if iFile == nil || jFile == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if iFile.HasMergeConflicts && !jFile.HasMergeConflicts {
|
||||
return true
|
||||
}
|
||||
|
||||
if jFile.HasMergeConflicts && !iFile.HasMergeConflicts {
|
||||
return false
|
||||
}
|
||||
|
||||
if iFile.Tracked && !jFile.Tracked {
|
||||
return true
|
||||
}
|
||||
|
||||
if jFile.Tracked && !iFile.Tracked {
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
return &FileNode{Children: sortedFiles}
|
||||
}
|
||||
|
||||
func split(str string) []string {
|
||||
return strings.Split(str, "/")
|
||||
}
|
||||
|
||||
func join(strs []string) string {
|
||||
return strings.Join(strs, "/")
|
||||
}
|
||||
|
||||
510
pkg/gui/filetree/build_tree_test.go
Normal file
510
pkg/gui/filetree/build_tree_test.go
Normal file
@@ -0,0 +1,510 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBuildTreeFromFiles(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
files []*models.File
|
||||
expected *FileNode
|
||||
}{
|
||||
{
|
||||
name: "no files",
|
||||
files: []*models.File{},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "files in same directory",
|
||||
files: []*models.File{
|
||||
{
|
||||
Name: "dir1/a",
|
||||
},
|
||||
{
|
||||
Name: "dir1/b",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
Path: "dir1",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "dir1/b"},
|
||||
Path: "dir1/b",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be compressed",
|
||||
files: []*models.File{
|
||||
{
|
||||
Name: "dir1/a",
|
||||
},
|
||||
{
|
||||
Name: "dir2/b",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
CompressionLevel: 1,
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "dir2/b"},
|
||||
Path: "dir2/b",
|
||||
CompressionLevel: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be sorted",
|
||||
files: []*models.File{
|
||||
{
|
||||
Name: "b",
|
||||
},
|
||||
{
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "a"},
|
||||
Path: "a",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "b"},
|
||||
Path: "b",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be sorted including a merge conflict file",
|
||||
files: []*models.File{
|
||||
{
|
||||
Name: "b",
|
||||
},
|
||||
{
|
||||
Name: "z",
|
||||
HasMergeConflicts: true,
|
||||
},
|
||||
{
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
// it is a little strange that we're not bubbling up our merge conflict
|
||||
// here but we are technically still in in tree mode and that's the rule
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "a"},
|
||||
Path: "a",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "b"},
|
||||
Path: "b",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "z", HasMergeConflicts: true},
|
||||
Path: "z",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
result := BuildTreeFromFiles(s.files)
|
||||
assert.EqualValues(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFlatTreeFromFiles(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
files []*models.File
|
||||
expected *FileNode
|
||||
}{
|
||||
{
|
||||
name: "no files",
|
||||
files: []*models.File{},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "files in same directory",
|
||||
files: []*models.File{
|
||||
{
|
||||
Name: "dir1/a",
|
||||
},
|
||||
{
|
||||
Name: "dir1/b",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
CompressionLevel: 0,
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "dir1/b"},
|
||||
Path: "dir1/b",
|
||||
CompressionLevel: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be compressed",
|
||||
files: []*models.File{
|
||||
{
|
||||
Name: "dir1/a",
|
||||
},
|
||||
{
|
||||
Name: "dir2/b",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
CompressionLevel: 1,
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "dir2/b"},
|
||||
Path: "dir2/b",
|
||||
CompressionLevel: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be sorted",
|
||||
files: []*models.File{
|
||||
{
|
||||
Name: "b",
|
||||
},
|
||||
{
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "a"},
|
||||
Path: "a",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "b"},
|
||||
Path: "b",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tracked, untracked, and conflicted files",
|
||||
files: []*models.File{
|
||||
{
|
||||
Name: "a2",
|
||||
Tracked: false,
|
||||
},
|
||||
{
|
||||
Name: "a1",
|
||||
Tracked: false,
|
||||
},
|
||||
{
|
||||
Name: "c2",
|
||||
HasMergeConflicts: true,
|
||||
},
|
||||
{
|
||||
Name: "c1",
|
||||
HasMergeConflicts: true,
|
||||
},
|
||||
{
|
||||
Name: "b2",
|
||||
Tracked: true,
|
||||
},
|
||||
{
|
||||
Name: "b1",
|
||||
Tracked: true,
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "c1", HasMergeConflicts: true},
|
||||
Path: "c1",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "c2", HasMergeConflicts: true},
|
||||
Path: "c2",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "b1", Tracked: true},
|
||||
Path: "b1",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "b2", Tracked: true},
|
||||
Path: "b2",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "a1", Tracked: false},
|
||||
Path: "a1",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "a2", Tracked: false},
|
||||
Path: "a2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
result := BuildFlatTreeFromFiles(s.files)
|
||||
assert.EqualValues(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildTreeFromCommitFiles(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
files []*models.CommitFile
|
||||
expected *CommitFileNode
|
||||
}{
|
||||
{
|
||||
name: "no files",
|
||||
files: []*models.CommitFile{},
|
||||
expected: &CommitFileNode{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "files in same directory",
|
||||
files: []*models.CommitFile{
|
||||
{
|
||||
Name: "dir1/a",
|
||||
},
|
||||
{
|
||||
Name: "dir1/b",
|
||||
},
|
||||
},
|
||||
expected: &CommitFileNode{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{
|
||||
{
|
||||
Path: "dir1",
|
||||
Children: []*CommitFileNode{
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
},
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir1/b"},
|
||||
Path: "dir1/b",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be compressed",
|
||||
files: []*models.CommitFile{
|
||||
{
|
||||
Name: "dir1/a",
|
||||
},
|
||||
{
|
||||
Name: "dir2/b",
|
||||
},
|
||||
},
|
||||
expected: &CommitFileNode{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
CompressionLevel: 1,
|
||||
},
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir2/b"},
|
||||
Path: "dir2/b",
|
||||
CompressionLevel: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be sorted",
|
||||
files: []*models.CommitFile{
|
||||
{
|
||||
Name: "b",
|
||||
},
|
||||
{
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
expected: &CommitFileNode{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{
|
||||
{
|
||||
File: &models.CommitFile{Name: "a"},
|
||||
Path: "a",
|
||||
},
|
||||
{
|
||||
File: &models.CommitFile{Name: "b"},
|
||||
Path: "b",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
result := BuildTreeFromCommitFiles(s.files)
|
||||
assert.EqualValues(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFlatTreeFromCommitFiles(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
files []*models.CommitFile
|
||||
expected *CommitFileNode
|
||||
}{
|
||||
{
|
||||
name: "no files",
|
||||
files: []*models.CommitFile{},
|
||||
expected: &CommitFileNode{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "files in same directory",
|
||||
files: []*models.CommitFile{
|
||||
{
|
||||
Name: "dir1/a",
|
||||
},
|
||||
{
|
||||
Name: "dir1/b",
|
||||
},
|
||||
},
|
||||
expected: &CommitFileNode{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
CompressionLevel: 0,
|
||||
},
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir1/b"},
|
||||
Path: "dir1/b",
|
||||
CompressionLevel: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be compressed",
|
||||
files: []*models.CommitFile{
|
||||
{
|
||||
Name: "dir1/a",
|
||||
},
|
||||
{
|
||||
Name: "dir2/b",
|
||||
},
|
||||
},
|
||||
expected: &CommitFileNode{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir1/a"},
|
||||
Path: "dir1/a",
|
||||
CompressionLevel: 1,
|
||||
},
|
||||
{
|
||||
File: &models.CommitFile{Name: "dir2/b"},
|
||||
Path: "dir2/b",
|
||||
CompressionLevel: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be sorted",
|
||||
files: []*models.CommitFile{
|
||||
{
|
||||
Name: "b",
|
||||
},
|
||||
{
|
||||
Name: "a",
|
||||
},
|
||||
},
|
||||
expected: &CommitFileNode{
|
||||
Path: "",
|
||||
Children: []*CommitFileNode{
|
||||
{
|
||||
File: &models.CommitFile{Name: "a"},
|
||||
Path: "a",
|
||||
},
|
||||
{
|
||||
File: &models.CommitFile{Name: "b"},
|
||||
Path: "b",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
result := BuildFlatTreeFromCommitFiles(s.files)
|
||||
assert.EqualValues(t, s.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,12 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type CollapsedPaths map[string]bool
|
||||
|
||||
func (cp CollapsedPaths) ExpandToPath(path string) {
|
||||
// need every directory along the way
|
||||
split := strings.Split(path, string(os.PathSeparator))
|
||||
for i := range split {
|
||||
dir := strings.Join(split[0:i+1], string(os.PathSeparator))
|
||||
splitPath := split(path)
|
||||
for i := range splitPath {
|
||||
dir := join(splitPath[0 : i+1])
|
||||
cp[dir] = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
package filetree
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
)
|
||||
|
||||
@@ -169,8 +165,8 @@ func (s *CommitFileNode) AnyFile(test func(file *models.CommitFile) bool) bool {
|
||||
}
|
||||
|
||||
func (s *CommitFileNode) NameAtDepth(depth int) string {
|
||||
splitName := strings.Split(s.Path, string(os.PathSeparator))
|
||||
name := filepath.Join(splitName[depth:]...)
|
||||
splitName := split(s.Path)
|
||||
name := join(splitName[depth:])
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
@@ -2,9 +2,6 @@ package filetree
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
)
|
||||
@@ -171,18 +168,18 @@ func (s *FileNode) AnyFile(test func(file *models.File) bool) bool {
|
||||
}
|
||||
|
||||
func (s *FileNode) NameAtDepth(depth int) string {
|
||||
splitName := strings.Split(s.Path, string(os.PathSeparator))
|
||||
name := filepath.Join(splitName[depth:]...)
|
||||
splitName := split(s.Path)
|
||||
name := join(splitName[depth:])
|
||||
|
||||
if s.File != nil && s.File.IsRename() {
|
||||
splitPrevName := strings.Split(s.File.PreviousName, string(os.PathSeparator))
|
||||
splitPrevName := split(s.File.PreviousName)
|
||||
|
||||
prevName := s.File.PreviousName
|
||||
// if the file has just been renamed inside the same directory, we can shave off
|
||||
// the prefix for the previous path too. Otherwise we'll keep it unchanged
|
||||
sameParentDir := len(splitName) == len(splitPrevName) && filepath.Join(splitName[0:depth]...) == filepath.Join(splitPrevName[0:depth]...)
|
||||
sameParentDir := len(splitName) == len(splitPrevName) && join(splitName[0:depth]) == join(splitPrevName[0:depth])
|
||||
if sameParentDir {
|
||||
prevName = filepath.Join(splitPrevName[depth:]...)
|
||||
prevName = join(splitPrevName[depth:])
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s%s%s", prevName, " → ", name)
|
||||
|
||||
@@ -31,8 +31,8 @@ func (gui *Gui) gitFlowFinishBranch(gitFlowConfig string, branchName string) err
|
||||
return gui.createErrorPanel(gui.Tr.NotAGitFlowBranch)
|
||||
}
|
||||
|
||||
return gui.runSubprocessWithSuspense(
|
||||
gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "finish", suffix),
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(
|
||||
gui.OSCommand.WithSpan(gui.Tr.Spans.GitFlowFinish).PrepareSubProcess("git", "flow", branchType, "finish", suffix),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -55,8 +55,8 @@ func (gui *Gui) handleCreateGitFlowMenu() error {
|
||||
return gui.prompt(promptOpts{
|
||||
title: title,
|
||||
handleConfirm: func(name string) error {
|
||||
return gui.runSubprocessWithSuspense(
|
||||
gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "start", name),
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(
|
||||
gui.OSCommand.WithSpan(gui.Tr.Spans.GitFlowStart).PrepareSubProcess("git", "flow", branchType, "start", name),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -68,6 +68,21 @@ func (gui *Gui) scrollUpView(view *gocui.View) error {
|
||||
|
||||
func (gui *Gui) scrollDownView(view *gocui.View) error {
|
||||
ox, oy := view.Origin()
|
||||
scrollHeight := gui.linesToScrollDown(view)
|
||||
if scrollHeight > 0 {
|
||||
if err := view.SetOrigin(ox, oy+scrollHeight); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if manager, ok := gui.viewBufferManagerMap[view.Name()]; ok {
|
||||
manager.ReadLines(scrollHeight)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) linesToScrollDown(view *gocui.View) int {
|
||||
_, oy := view.Origin()
|
||||
y := oy
|
||||
canScrollPastBottom := gui.Config.GetUserConfig().Gui.ScrollPastBottom
|
||||
if !canScrollPastBottom {
|
||||
@@ -76,27 +91,25 @@ func (gui *Gui) scrollDownView(view *gocui.View) error {
|
||||
}
|
||||
scrollHeight := gui.Config.GetUserConfig().Gui.ScrollHeight
|
||||
scrollableLines := view.ViewLinesHeight() - y
|
||||
if scrollableLines > 0 {
|
||||
// margin is about how many lines must still appear if you scroll
|
||||
// all the way down. In practice every file ends in a newline so it will really
|
||||
// just show a single line
|
||||
margin := 1
|
||||
if canScrollPastBottom {
|
||||
margin = 2
|
||||
}
|
||||
if scrollableLines-margin < scrollHeight {
|
||||
scrollHeight = scrollableLines - margin
|
||||
}
|
||||
if oy+scrollHeight >= 0 {
|
||||
if err := view.SetOrigin(ox, oy+scrollHeight); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if scrollableLines < 0 {
|
||||
return 0
|
||||
}
|
||||
if manager, ok := gui.viewBufferManagerMap[view.Name()]; ok {
|
||||
manager.ReadLines(scrollHeight)
|
||||
|
||||
// margin is about how many lines must still appear if you scroll
|
||||
// all the way down. In practice every file ends in a newline so it will really
|
||||
// just show a single line
|
||||
margin := 1
|
||||
if canScrollPastBottom {
|
||||
margin = 2
|
||||
}
|
||||
if scrollableLines-margin < scrollHeight {
|
||||
scrollHeight = scrollableLines - margin
|
||||
}
|
||||
if oy+scrollHeight < 0 {
|
||||
return 0
|
||||
} else {
|
||||
return scrollHeight
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) scrollUpMain() error {
|
||||
@@ -148,13 +161,13 @@ func (gui *Gui) handleMouseDownMain() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch gui.g.CurrentView() {
|
||||
case gui.Views.Files:
|
||||
switch gui.currentSideContext() {
|
||||
case gui.State.Contexts.Files:
|
||||
// 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())
|
||||
case gui.Views.CommitFiles:
|
||||
case gui.State.Contexts.CommitFiles:
|
||||
return gui.enterCommitFile(gui.Views.Main.SelectedLineIdx())
|
||||
}
|
||||
|
||||
@@ -174,33 +187,7 @@ func (gui *Gui) handleMouseDownSecondary() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleInfoClick() error {
|
||||
if !gui.g.Mouse {
|
||||
return nil
|
||||
}
|
||||
|
||||
view := gui.Views.Information
|
||||
|
||||
cx, _ := view.Cursor()
|
||||
width, _ := view.Size()
|
||||
|
||||
for _, mode := range gui.modeStatuses() {
|
||||
if mode.isActive() {
|
||||
if width-cx > len(gui.Tr.ResetInParentheses) {
|
||||
return nil
|
||||
}
|
||||
return mode.reset()
|
||||
}
|
||||
}
|
||||
|
||||
// if we're not in an active mode we show the donate button
|
||||
if cx <= len(gui.Tr.Donate)+len(INFO_SECTION_PADDING) {
|
||||
return gui.OSCommand.OpenLink("https://github.com/sponsors/jesseduffield")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) fetch(canPromptForCredentials bool) (err error) {
|
||||
func (gui *Gui) fetch(canPromptForCredentials bool, span string) (err error) {
|
||||
gui.Mutexes.FetchMutex.Lock()
|
||||
defer gui.Mutexes.FetchMutex.Unlock()
|
||||
|
||||
@@ -209,7 +196,7 @@ func (gui *Gui) fetch(canPromptForCredentials bool) (err error) {
|
||||
fetchOpts.PromptUserForCredential = gui.promptUserForCredential
|
||||
}
|
||||
|
||||
err = gui.GitCommand.Fetch(fetchOpts)
|
||||
err = gui.GitCommand.WithSpan(span).Fetch(fetchOpts)
|
||||
|
||||
if canPromptForCredentials && err != nil && strings.Contains(err.Error(), "exit status 128") {
|
||||
_ = gui.createErrorPanel(gui.Tr.PassUnameWrong)
|
||||
@@ -228,7 +215,7 @@ func (gui *Gui) handleCopySelectedSideContextItemToClipboard() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := gui.OSCommand.CopyToClipboard(itemId); err != nil {
|
||||
if err := gui.OSCommand.WithSpan(gui.Tr.Spans.CopyToClipboard).CopyToClipboard(itemId); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
|
||||
41
pkg/gui/gpg.go
Normal file
41
pkg/gui/gpg.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package gui
|
||||
|
||||
// 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 {
|
||||
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))
|
||||
if success && onSuccess != nil {
|
||||
if err := onSuccess(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
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 nil
|
||||
}
|
||||
121
pkg/gui/gui.go
121
pkg/gui/gui.go
@@ -13,14 +13,14 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/golang-collections/collections/stack"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"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/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/lbl"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/modes/filtering"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
@@ -44,11 +44,15 @@ const (
|
||||
SCREEN_FULL
|
||||
)
|
||||
|
||||
const StartupPopupVersion = 3
|
||||
const StartupPopupVersion = 5
|
||||
|
||||
// OverlappingEdges determines if panel edges overlap
|
||||
var OverlappingEdges = false
|
||||
|
||||
func init() {
|
||||
runewidth.DefaultCondition.EastAsianWidth = false
|
||||
}
|
||||
|
||||
type ContextManager struct {
|
||||
ContextStack []Context
|
||||
sync.RWMutex
|
||||
@@ -90,7 +94,7 @@ type Gui struct {
|
||||
// recent repo with the recent repos popup showing
|
||||
showRecentRepos bool
|
||||
|
||||
Mutexes guiStateMutexes
|
||||
Mutexes guiMutexes
|
||||
|
||||
// findSuggestions will take a string that the user has typed into a prompt
|
||||
// and return a slice of suggestions which match that string.
|
||||
@@ -104,6 +108,18 @@ type Gui struct {
|
||||
ViewsSetup bool
|
||||
|
||||
Views Views
|
||||
|
||||
// if we've suspended the gui (e.g. because we've switched to a subprocess)
|
||||
// we typically want to pause some things that are running like background
|
||||
// file refreshes
|
||||
PauseBackgroundThreads bool
|
||||
|
||||
// Log of the commands that get run, to be displayed to the user.
|
||||
CmdLog []string
|
||||
OnRunCommand func(entry oscommands.CmdLogEntry)
|
||||
|
||||
// the extras window contains things like the command log
|
||||
ShowExtrasWindow bool
|
||||
}
|
||||
|
||||
type listPanelState struct {
|
||||
@@ -121,22 +137,13 @@ func (h *listPanelState) GetSelectedLineIdx() int {
|
||||
// for now the staging panel state, unlike the other panel states, is going to be
|
||||
// non-mutative, so that we don't accidentally end up
|
||||
// with mismatches of data. We might change this in the future
|
||||
type lBlPanelState struct {
|
||||
SelectedLineIdx int
|
||||
FirstLineIdx int
|
||||
LastLineIdx int
|
||||
Diff string
|
||||
PatchParser *patch.PatchParser
|
||||
SelectMode SelectMode
|
||||
type LblPanelState struct {
|
||||
*lbl.State
|
||||
SecondaryFocused bool // this is for if we show the left or right panel
|
||||
}
|
||||
|
||||
type mergingPanelState struct {
|
||||
ConflictIndex int
|
||||
ConflictTop bool
|
||||
Conflicts []commands.Conflict
|
||||
ConflictsMutex sync.Mutex
|
||||
EditHistory *stack.Stack
|
||||
type MergingPanelState struct {
|
||||
*mergeconflicts.State
|
||||
|
||||
// UserScrolling tells us if the user has started scrolling through the file themselves
|
||||
// in which case we won't auto-scroll to a conflict.
|
||||
@@ -218,8 +225,8 @@ type panelStates struct {
|
||||
SubCommits *subCommitPanelState
|
||||
Stash *stashPanelState
|
||||
Menu *menuPanelState
|
||||
LineByLine *lBlPanelState
|
||||
Merging *mergingPanelState
|
||||
LineByLine *LblPanelState
|
||||
Merging *MergingPanelState
|
||||
CommitFiles *commitFilesPanelState
|
||||
Submodules *submodulePanelState
|
||||
Suggestions *suggestionsPanelState
|
||||
@@ -245,6 +252,7 @@ type Views struct {
|
||||
SearchPrefix *gocui.View
|
||||
Limit *gocui.View
|
||||
Suggestions *gocui.View
|
||||
Extras *gocui.View
|
||||
}
|
||||
|
||||
type searchingState struct {
|
||||
@@ -288,12 +296,13 @@ type Modes struct {
|
||||
Diffing Diffing
|
||||
}
|
||||
|
||||
type guiStateMutexes struct {
|
||||
type guiMutexes struct {
|
||||
RefreshingFilesMutex sync.Mutex
|
||||
RefreshingStatusMutex sync.Mutex
|
||||
FetchMutex sync.Mutex
|
||||
BranchCommitsMutex sync.Mutex
|
||||
LineByLinePanelMutex sync.Mutex
|
||||
SubprocessMutex sync.Mutex
|
||||
}
|
||||
|
||||
type guiState struct {
|
||||
@@ -327,7 +336,6 @@ type guiState struct {
|
||||
IsRefreshingFiles bool
|
||||
Searching searchingState
|
||||
ScreenMode WindowMaximisation
|
||||
SideView *gocui.View
|
||||
Ptmx *os.File
|
||||
PrevMainWidth int
|
||||
PrevMainHeight int
|
||||
@@ -409,16 +417,12 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
|
||||
Stash: &stashPanelState{listPanelState{SelectedLineIdx: -1}},
|
||||
Menu: &menuPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}, OnPress: nil},
|
||||
Suggestions: &suggestionsPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}},
|
||||
Merging: &mergingPanelState{
|
||||
ConflictIndex: 0,
|
||||
ConflictTop: true,
|
||||
Conflicts: []commands.Conflict{},
|
||||
EditHistory: stack.New(),
|
||||
ConflictsMutex: sync.Mutex{},
|
||||
Merging: &MergingPanelState{
|
||||
State: mergeconflicts.NewState(),
|
||||
UserScrolling: false,
|
||||
},
|
||||
},
|
||||
SideView: nil,
|
||||
Ptmx: nil,
|
||||
Ptmx: nil,
|
||||
Modes: Modes{
|
||||
Filtering: filtering.NewFiltering(filterPath),
|
||||
CherryPicking: CherryPicking{
|
||||
@@ -453,12 +457,18 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *oscom
|
||||
showRecentRepos: showRecentRepos,
|
||||
RepoPathStack: []string{},
|
||||
RepoStateMap: map[Repo]*guiState{},
|
||||
CmdLog: []string{},
|
||||
ShowExtrasWindow: config.GetUserConfig().Gui.ShowCommandLog,
|
||||
}
|
||||
|
||||
gui.resetState(filterPath, false)
|
||||
|
||||
gui.watchFilesForChanges()
|
||||
|
||||
onRunCommand := gui.GetOnRunCommand()
|
||||
oSCommand.SetOnRunCommand(onRunCommand)
|
||||
gui.OnRunCommand = onRunCommand
|
||||
|
||||
return gui, nil
|
||||
}
|
||||
|
||||
@@ -476,6 +486,7 @@ func (gui *Gui) Run() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.g = g // TODO: always use gui.g rather than passing g around everywhere
|
||||
defer g.Close()
|
||||
|
||||
@@ -515,15 +526,6 @@ func (gui *Gui) Run() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if !gui.Config.GetUserConfig().DisableStartupPopups {
|
||||
popupTasks := []func(chan struct{}) error{}
|
||||
storedPopupVersion := gui.Config.GetAppState().StartupPopupVersion
|
||||
if storedPopupVersion < StartupPopupVersion {
|
||||
popupTasks = append(popupTasks, gui.showIntroPopupMessage)
|
||||
}
|
||||
gui.showInitialPopups(popupTasks)
|
||||
}
|
||||
|
||||
gui.waitForIntro.Add(1)
|
||||
if gui.Config.GetUserConfig().Git.AutoFetch {
|
||||
go utils.Safe(gui.startBackgroundFetch)
|
||||
@@ -577,7 +579,25 @@ func (gui *Gui) RunAndHandleError() error {
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) runSubprocessWithSuspense(subprocess *exec.Cmd) error {
|
||||
// returns whether command exited without error or not
|
||||
func (gui *Gui) runSubprocessWithSuspenseAndRefresh(subprocess *exec.Cmd) error {
|
||||
_, err := gui.runSubprocessWithSuspense(subprocess)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// returns whether command exited without error or not
|
||||
func (gui *Gui) runSubprocessWithSuspense(subprocess *exec.Cmd) (bool, error) {
|
||||
gui.Mutexes.SubprocessMutex.Lock()
|
||||
defer gui.Mutexes.SubprocessMutex.Unlock()
|
||||
|
||||
if replaying() {
|
||||
// we do not yet support running subprocesses within integration tests. So if
|
||||
// we're replaying an integration test and we're inside this method, something
|
||||
@@ -586,21 +606,21 @@ func (gui *Gui) runSubprocessWithSuspense(subprocess *exec.Cmd) error {
|
||||
log.Fatal("opening subprocesses not yet supported in integration tests. Chances are that this test is running too fast and a subprocess is accidentally opened")
|
||||
}
|
||||
|
||||
if err := gocui.Screen.Suspend(); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
if err := gui.g.Suspend(); err != nil {
|
||||
return false, gui.surfaceError(err)
|
||||
}
|
||||
|
||||
gui.PauseBackgroundThreads = true
|
||||
|
||||
cmdErr := gui.runSubprocess(subprocess)
|
||||
|
||||
if err := gocui.Screen.Resume(); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
if err := gui.g.Resume(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := gui.refreshSidePanels(refreshOptions{mode: ASYNC}); err != nil {
|
||||
return err
|
||||
}
|
||||
gui.PauseBackgroundThreads = false
|
||||
|
||||
return gui.surfaceError(cmdErr)
|
||||
return cmdErr == nil, gui.surfaceError(cmdErr)
|
||||
}
|
||||
|
||||
func (gui *Gui) runSubprocess(subprocess *exec.Cmd) error {
|
||||
@@ -679,6 +699,9 @@ func (gui *Gui) goEvery(interval time.Duration, stop chan struct{}, function fun
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if gui.PauseBackgroundThreads {
|
||||
continue
|
||||
}
|
||||
_ = function()
|
||||
case <-stop:
|
||||
return
|
||||
@@ -694,7 +717,7 @@ func (gui *Gui) startBackgroundFetch() {
|
||||
if !isNew {
|
||||
time.After(time.Duration(userConfig.Refresher.FetchInterval) * time.Second)
|
||||
}
|
||||
err := gui.fetch(false)
|
||||
err := gui.fetch(false, "")
|
||||
if err != nil && strings.Contains(err.Error(), "exit status 128") && isNew {
|
||||
_ = gui.ask(askOpts{
|
||||
title: gui.Tr.NoAutomaticGitFetchTitle,
|
||||
@@ -702,7 +725,7 @@ func (gui *Gui) startBackgroundFetch() {
|
||||
})
|
||||
} else {
|
||||
gui.goEvery(time.Second*time.Duration(userConfig.Refresher.FetchInterval), gui.stopChan, func() error {
|
||||
err := gui.fetch(false)
|
||||
err := gui.fetch(false, "")
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// +build !windows
|
||||
|
||||
package gui
|
||||
|
||||
import (
|
||||
|
||||
52
pkg/gui/information_panel.go
Normal file
52
pkg/gui/information_panel.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/constants"
|
||||
)
|
||||
|
||||
func (gui *Gui) informationStr() string {
|
||||
for _, mode := range gui.modeStatuses() {
|
||||
if mode.isActive() {
|
||||
return mode.description()
|
||||
}
|
||||
}
|
||||
|
||||
if gui.g.Mouse {
|
||||
donate := color.New(color.FgMagenta, color.Underline).Sprint(gui.Tr.Donate)
|
||||
askQuestion := color.New(color.FgYellow, color.Underline).Sprint(gui.Tr.AskQuestion)
|
||||
return fmt.Sprintf("%s %s %s", donate, askQuestion, gui.Config.GetVersion())
|
||||
} else {
|
||||
return gui.Config.GetVersion()
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) handleInfoClick() error {
|
||||
if !gui.g.Mouse {
|
||||
return nil
|
||||
}
|
||||
|
||||
view := gui.Views.Information
|
||||
|
||||
cx, _ := view.Cursor()
|
||||
width, _ := view.Size()
|
||||
|
||||
for _, mode := range gui.modeStatuses() {
|
||||
if mode.isActive() {
|
||||
if width-cx > len(gui.Tr.ResetInParentheses) {
|
||||
return nil
|
||||
}
|
||||
return mode.reset()
|
||||
}
|
||||
}
|
||||
|
||||
// if we're not in an active mode we show the donate button
|
||||
if cx <= len(gui.Tr.Donate) {
|
||||
return gui.OSCommand.OpenLink(constants.Links.Donate)
|
||||
} else if cx <= len(gui.Tr.Donate)+1+len(gui.Tr.AskQuestion) {
|
||||
return gui.OSCommand.OpenLink(constants.Links.Discussions)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/constants"
|
||||
)
|
||||
|
||||
// Binding - a keybinding mapping a key and modifier to a handler. The keypress
|
||||
@@ -53,7 +54,8 @@ var keyMapReversed = map[gocui.Key]string{
|
||||
gocui.KeyArrowDown: "▼",
|
||||
gocui.KeyArrowLeft: "◄",
|
||||
gocui.KeyArrowRight: "►",
|
||||
gocui.KeyTab: "tab", // ctrl+i
|
||||
gocui.KeyTab: "tab", // ctrl+i
|
||||
gocui.KeyBacktab: "shift+tab",
|
||||
gocui.KeyEnter: "enter", // ctrl+m
|
||||
gocui.KeyAltEnter: "alt+enter",
|
||||
gocui.KeyEsc: "esc", // ctrl+[, ctrl+3
|
||||
@@ -133,6 +135,7 @@ var keymap = map[string]interface{}{
|
||||
"<c-_>": gocui.KeyCtrlUnderscore,
|
||||
"<backspace>": gocui.KeyBackspace,
|
||||
"<tab>": gocui.KeyTab,
|
||||
"<backtab>": gocui.KeyBacktab,
|
||||
"<enter>": gocui.KeyEnter,
|
||||
"<a-enter>": gocui.KeyAltEnter,
|
||||
"<esc>": gocui.KeyEsc,
|
||||
@@ -188,7 +191,7 @@ func (gui *Gui) getKey(key string) interface{} {
|
||||
if runeCount > 1 {
|
||||
binding := keymap[strings.ToLower(key)]
|
||||
if binding == nil {
|
||||
log.Fatalf("Unrecognized key %s for keybinding. For permitted values see https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md", strings.ToLower(key))
|
||||
log.Fatalf("Unrecognized key %s for keybinding. For permitted values see %s", strings.ToLower(key), constants.Links.Docs.CustomKeybindings)
|
||||
} else {
|
||||
return binding
|
||||
}
|
||||
@@ -228,6 +231,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleTopLevelReturn,
|
||||
},
|
||||
{
|
||||
ViewName: "",
|
||||
Key: gui.getKey(config.Universal.OpenRecentRepos),
|
||||
Handler: gui.handleCreateRecentReposMenu,
|
||||
Alternative: "<c-r>",
|
||||
Description: gui.Tr.SwitchRepo,
|
||||
},
|
||||
{
|
||||
ViewName: "",
|
||||
Key: gui.getKey(config.Universal.ScrollUpMain),
|
||||
@@ -514,6 +524,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
Handler: gui.handleToggleFileTreeView,
|
||||
Description: gui.Tr.LcToggleTreeView,
|
||||
},
|
||||
{
|
||||
ViewName: "files",
|
||||
Contexts: []string{string(FILES_CONTEXT_KEY)},
|
||||
Key: gui.getKey(config.Files.OpenMergeTool),
|
||||
Handler: gui.handleOpenMergeTool,
|
||||
Description: gui.Tr.LcOpenMergeTool,
|
||||
},
|
||||
{
|
||||
ViewName: "branches",
|
||||
Contexts: []string{string(LOCAL_BRANCHES_CONTEXT_KEY)},
|
||||
@@ -1104,6 +1121,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
Description: gui.Tr.LcOpenDiffingMenu,
|
||||
OpensMenu: true,
|
||||
},
|
||||
{
|
||||
ViewName: "",
|
||||
Key: gui.getKey(config.Universal.ExtrasMenu),
|
||||
Handler: gui.handleCreateExtrasMenuPanel,
|
||||
Description: gui.Tr.LcOpenExtrasMenu,
|
||||
OpensMenu: true,
|
||||
},
|
||||
{
|
||||
ViewName: "secondary",
|
||||
Key: gocui.MouseWheelUp,
|
||||
@@ -1408,6 +1432,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
Handler: gui.handleEscapeMerge,
|
||||
Description: gui.Tr.ReturnToFilesPanel,
|
||||
},
|
||||
{
|
||||
ViewName: "main",
|
||||
Contexts: []string{string(MAIN_MERGING_CONTEXT_KEY)},
|
||||
Key: gui.getKey(config.Files.OpenMergeTool),
|
||||
Handler: gui.handleOpenMergeTool,
|
||||
Description: gui.Tr.LcOpenMergeTool,
|
||||
},
|
||||
{
|
||||
ViewName: "main",
|
||||
Contexts: []string{string(MAIN_MERGING_CONTEXT_KEY)},
|
||||
@@ -1688,6 +1719,62 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
Description: gui.Tr.LcViewBulkSubmoduleOptions,
|
||||
OpensMenu: true,
|
||||
},
|
||||
{
|
||||
ViewName: "extras",
|
||||
Key: gocui.MouseWheelUp,
|
||||
Handler: gui.scrollUpExtra,
|
||||
},
|
||||
{
|
||||
ViewName: "extras",
|
||||
Key: gocui.MouseWheelDown,
|
||||
Handler: gui.scrollDownExtra,
|
||||
},
|
||||
{
|
||||
ViewName: "extras",
|
||||
Key: gui.getKey(config.Universal.ExtrasMenu),
|
||||
Handler: gui.handleCreateExtrasMenuPanel,
|
||||
Description: gui.Tr.LcOpenExtrasMenu,
|
||||
OpensMenu: true,
|
||||
},
|
||||
{
|
||||
ViewName: "extras",
|
||||
Tag: "navigation",
|
||||
Contexts: []string{string(COMMAND_LOG_CONTEXT_KEY)},
|
||||
Key: gui.getKey(config.Universal.PrevItemAlt),
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.scrollUpExtra,
|
||||
},
|
||||
{
|
||||
ViewName: "extras",
|
||||
Tag: "navigation",
|
||||
Contexts: []string{string(COMMAND_LOG_CONTEXT_KEY)},
|
||||
Key: gui.getKey(config.Universal.PrevItem),
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.scrollUpExtra,
|
||||
},
|
||||
{
|
||||
ViewName: "extras",
|
||||
Tag: "navigation",
|
||||
Contexts: []string{string(COMMAND_LOG_CONTEXT_KEY)},
|
||||
Key: gui.getKey(config.Universal.NextItem),
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.scrollDownExtra,
|
||||
},
|
||||
{
|
||||
ViewName: "extras",
|
||||
Tag: "navigation",
|
||||
Contexts: []string{string(COMMAND_LOG_CONTEXT_KEY)},
|
||||
Key: gui.getKey(config.Universal.NextItemAlt),
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.scrollDownExtra,
|
||||
},
|
||||
{
|
||||
ViewName: "extras",
|
||||
Tag: "navigation",
|
||||
Key: gocui.MouseLeft,
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleFocusCommandLog,
|
||||
},
|
||||
}
|
||||
|
||||
for _, viewName := range []string{"status", "branches", "files", "commits", "commitFiles", "stash", "menu"} {
|
||||
@@ -1696,8 +1783,8 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
|
||||
{ViewName: viewName, Key: gui.getKey(config.Universal.NextBlock), Modifier: gocui.ModNone, Handler: gui.nextSideWindow},
|
||||
{ViewName: viewName, Key: gui.getKey(config.Universal.PrevBlockAlt), Modifier: gocui.ModNone, Handler: gui.previousSideWindow},
|
||||
{ViewName: viewName, Key: gui.getKey(config.Universal.NextBlockAlt), Modifier: gocui.ModNone, Handler: gui.nextSideWindow},
|
||||
{ViewName: viewName, Key: gocui.KeyBacktab, Modifier: gocui.ModNone, Handler: gui.previousSideWindow},
|
||||
{ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.nextSideWindow},
|
||||
{ViewName: viewName, Key: gui.getKey(config.Universal.PrevBlockAlt2), Modifier: gocui.ModNone, Handler: gui.previousSideWindow},
|
||||
{ViewName: viewName, Key: gui.getKey(config.Universal.NextBlockAlt2), Modifier: gocui.ModNone, Handler: gui.nextSideWindow},
|
||||
}...)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,44 +1,153 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
)
|
||||
|
||||
const SEARCH_PREFIX = "search: "
|
||||
const INFO_SECTION_PADDING = " "
|
||||
|
||||
func (gui *Gui) informationStr() string {
|
||||
for _, mode := range gui.modeStatuses() {
|
||||
if mode.isActive() {
|
||||
return mode.description()
|
||||
func (gui *Gui) createAllViews() error {
|
||||
viewNameMappings := []struct {
|
||||
viewPtr **gocui.View
|
||||
name string
|
||||
}{
|
||||
{viewPtr: &gui.Views.Status, name: "status"},
|
||||
{viewPtr: &gui.Views.Files, name: "files"},
|
||||
{viewPtr: &gui.Views.Branches, name: "branches"},
|
||||
{viewPtr: &gui.Views.Commits, name: "commits"},
|
||||
{viewPtr: &gui.Views.Stash, name: "stash"},
|
||||
{viewPtr: &gui.Views.CommitFiles, name: "commitFiles"},
|
||||
{viewPtr: &gui.Views.Main, name: "main"},
|
||||
{viewPtr: &gui.Views.Secondary, name: "secondary"},
|
||||
{viewPtr: &gui.Views.Options, name: "options"},
|
||||
{viewPtr: &gui.Views.AppStatus, name: "appStatus"},
|
||||
{viewPtr: &gui.Views.Information, name: "information"},
|
||||
{viewPtr: &gui.Views.Search, name: "search"},
|
||||
{viewPtr: &gui.Views.SearchPrefix, name: "searchPrefix"},
|
||||
{viewPtr: &gui.Views.CommitMessage, name: "commitMessage"},
|
||||
{viewPtr: &gui.Views.Credentials, name: "credentials"},
|
||||
{viewPtr: &gui.Views.Menu, name: "menu"},
|
||||
{viewPtr: &gui.Views.Suggestions, name: "suggestions"},
|
||||
{viewPtr: &gui.Views.Confirmation, name: "confirmation"},
|
||||
{viewPtr: &gui.Views.Limit, name: "limit"},
|
||||
{viewPtr: &gui.Views.Extras, name: "extras"},
|
||||
}
|
||||
|
||||
var err error
|
||||
for _, mapping := range viewNameMappings {
|
||||
*mapping.viewPtr, err = gui.prepareView(mapping.name)
|
||||
if err != nil && err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if gui.g.Mouse {
|
||||
donate := color.New(color.FgMagenta, color.Underline).Sprint(gui.Tr.Donate)
|
||||
return donate + " " + gui.Config.GetVersion()
|
||||
} else {
|
||||
return gui.Config.GetVersion()
|
||||
gui.Views.Options.Frame = false
|
||||
gui.Views.Options.FgColor = theme.OptionsColor
|
||||
|
||||
gui.Views.SearchPrefix.BgColor = gocui.ColorDefault
|
||||
gui.Views.SearchPrefix.FgColor = gocui.ColorGreen
|
||||
gui.Views.SearchPrefix.Frame = false
|
||||
gui.setViewContentSync(gui.Views.SearchPrefix, SEARCH_PREFIX)
|
||||
|
||||
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
|
||||
gui.Views.Secondary.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.Secondary.IgnoreCarriageReturns = true
|
||||
|
||||
gui.Views.Main.Title = gui.Tr.DiffTitle
|
||||
gui.Views.Main.Wrap = true
|
||||
gui.Views.Main.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.Main.IgnoreCarriageReturns = true
|
||||
|
||||
gui.Views.Limit.Title = gui.Tr.NotEnoughSpace
|
||||
gui.Views.Limit.Wrap = true
|
||||
|
||||
gui.Views.Status.Title = gui.Tr.StatusTitle
|
||||
gui.Views.Status.FgColor = theme.GocuiDefaultTextColor
|
||||
|
||||
gui.Views.Search.BgColor = gocui.ColorDefault
|
||||
gui.Views.Search.FgColor = gocui.ColorGreen
|
||||
gui.Views.Search.Frame = false
|
||||
gui.Views.Search.Editable = true
|
||||
|
||||
gui.Views.AppStatus.BgColor = gocui.ColorDefault
|
||||
gui.Views.AppStatus.FgColor = gocui.ColorCyan
|
||||
gui.Views.AppStatus.Frame = false
|
||||
gui.Views.AppStatus.Visible = false
|
||||
|
||||
gui.Views.CommitMessage.Visible = false
|
||||
gui.Views.CommitMessage.Title = gui.Tr.CommitMessage
|
||||
gui.Views.CommitMessage.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.CommitMessage.Editable = true
|
||||
gui.Views.CommitMessage.Editor = gocui.EditorFunc(gui.commitMessageEditor)
|
||||
|
||||
gui.Views.Confirmation.Visible = false
|
||||
|
||||
gui.Views.Credentials.Visible = false
|
||||
gui.Views.Credentials.Title = gui.Tr.CredentialsUsername
|
||||
gui.Views.Credentials.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.Credentials.Editable = true
|
||||
|
||||
gui.Views.Suggestions.Visible = false
|
||||
|
||||
gui.Views.Menu.Visible = false
|
||||
|
||||
gui.Views.Information.BgColor = gocui.ColorDefault
|
||||
gui.Views.Information.FgColor = gocui.ColorGreen
|
||||
gui.Views.Information.Frame = false
|
||||
|
||||
gui.Views.Extras.Title = gui.Tr.CommandLog
|
||||
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
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// layout is called for every screen re-render e.g. when the screen is resized
|
||||
func (gui *Gui) layout(g *gocui.Gui) error {
|
||||
if !gui.ViewsSetup {
|
||||
if err := gui.createAllViews(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
g.Highlight = true
|
||||
width, height := g.Size()
|
||||
|
||||
minimumHeight := 9
|
||||
minimumWidth := 10
|
||||
var err error
|
||||
gui.Views.Limit, err = g.SetView("limit", 0, 0, width-1, height-1, 0)
|
||||
if err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
gui.Views.Limit.Title = gui.Tr.NotEnoughSpace
|
||||
gui.Views.Limit.Wrap = true
|
||||
_, err = g.SetView("limit", 0, 0, width-1, height-1, 0)
|
||||
if err != nil && err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
gui.Views.Limit.Visible = height < minimumHeight || width < minimumWidth
|
||||
|
||||
@@ -98,142 +207,39 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
||||
return view, err
|
||||
}
|
||||
|
||||
gui.Views.Main, err = setViewFromDimensions("main", "main", true)
|
||||
if err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
gui.Views.Main.Title = gui.Tr.DiffTitle
|
||||
gui.Views.Main.Wrap = true
|
||||
gui.Views.Main.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.Main.IgnoreCarriageReturns = true
|
||||
args := []struct {
|
||||
viewName string
|
||||
windowName string
|
||||
frame bool
|
||||
}{
|
||||
{viewName: "main", windowName: "main", frame: true},
|
||||
{viewName: "secondary", windowName: "secondary", frame: true},
|
||||
{viewName: "status", windowName: "status", frame: true},
|
||||
{viewName: "files", windowName: "files", frame: true},
|
||||
{viewName: "branches", windowName: "branches", frame: true},
|
||||
{viewName: "commitFiles", windowName: gui.State.Contexts.CommitFiles.GetWindowName(), frame: true},
|
||||
{viewName: "commits", windowName: "commits", frame: true},
|
||||
{viewName: "stash", windowName: "stash", frame: true},
|
||||
{viewName: "options", windowName: "options", frame: false},
|
||||
{viewName: "searchPrefix", windowName: "searchPrefix", frame: false},
|
||||
{viewName: "search", windowName: "search", frame: false},
|
||||
{viewName: "appStatus", windowName: "appStatus", frame: false},
|
||||
{viewName: "information", windowName: "information", frame: false},
|
||||
{viewName: "extras", windowName: "extras", frame: true},
|
||||
}
|
||||
|
||||
gui.Views.Secondary, err = setViewFromDimensions("secondary", "secondary", true)
|
||||
if err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
for _, arg := range args {
|
||||
_, err = setViewFromDimensions(arg.viewName, arg.windowName, arg.frame)
|
||||
if err != nil && err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
gui.Views.Secondary.Title = gui.Tr.DiffTitle
|
||||
gui.Views.Secondary.Wrap = true
|
||||
gui.Views.Secondary.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.Secondary.IgnoreCarriageReturns = true
|
||||
}
|
||||
|
||||
if gui.Views.Status, err = setViewFromDimensions("status", "status", true); err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
gui.Views.Status.Title = gui.Tr.StatusTitle
|
||||
gui.Views.Status.FgColor = theme.GocuiDefaultTextColor
|
||||
}
|
||||
|
||||
gui.Views.Files, err = setViewFromDimensions("files", "files", true)
|
||||
if err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
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.Branches, err = setViewFromDimensions("branches", "branches", true)
|
||||
if err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
gui.Views.Branches.Title = gui.Tr.BranchesTitle
|
||||
gui.Views.Branches.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.Branches.ContainsList = true
|
||||
}
|
||||
|
||||
gui.Views.CommitFiles, err = setViewFromDimensions("commitFiles", gui.State.Contexts.CommitFiles.GetWindowName(), true)
|
||||
if err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
gui.Views.CommitFiles.Title = gui.Tr.CommitFiles
|
||||
gui.Views.CommitFiles.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.CommitFiles.ContainsList = true
|
||||
}
|
||||
// if the commit files view is the view to be displayed for its window, we'll display it
|
||||
gui.Views.CommitFiles.Visible = gui.getViewNameForWindow(gui.State.Contexts.CommitFiles.GetWindowName()) == "commitFiles"
|
||||
|
||||
gui.Views.Commits, err = setViewFromDimensions("commits", "commits", true)
|
||||
if err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
gui.Views.Commits.Title = gui.Tr.CommitsTitle
|
||||
gui.Views.Commits.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.Commits.ContainsList = true
|
||||
}
|
||||
|
||||
gui.Views.Stash, err = setViewFromDimensions("stash", "stash", true)
|
||||
if err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
gui.Views.Stash.Title = gui.Tr.StashTitle
|
||||
gui.Views.Stash.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.Stash.ContainsList = true
|
||||
}
|
||||
|
||||
if gui.Views.Options, err = setViewFromDimensions("options", "options", false); err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
gui.Views.Options.Frame = false
|
||||
gui.Views.Options.FgColor = theme.OptionsColor
|
||||
}
|
||||
|
||||
// this view takes up one character. Its only purpose is to show the slash when searching
|
||||
if gui.Views.SearchPrefix, err = setViewFromDimensions("searchPrefix", "searchPrefix", false); err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.Views.SearchPrefix.BgColor = gocui.ColorDefault
|
||||
gui.Views.SearchPrefix.FgColor = gocui.ColorGreen
|
||||
gui.Views.SearchPrefix.Frame = false
|
||||
gui.setViewContent(gui.Views.SearchPrefix, SEARCH_PREFIX)
|
||||
}
|
||||
|
||||
if gui.Views.Search, err = setViewFromDimensions("search", "search", false); err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
|
||||
gui.Views.Search.BgColor = gocui.ColorDefault
|
||||
gui.Views.Search.FgColor = gocui.ColorGreen
|
||||
gui.Views.Search.Frame = false
|
||||
gui.Views.Search.Editable = true
|
||||
}
|
||||
|
||||
if gui.Views.AppStatus, err = setViewFromDimensions("appStatus", "appStatus", false); err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
gui.Views.AppStatus.BgColor = gocui.ColorDefault
|
||||
gui.Views.AppStatus.FgColor = gocui.ColorCyan
|
||||
gui.Views.AppStatus.Frame = false
|
||||
gui.Views.AppStatus.Visible = false
|
||||
}
|
||||
|
||||
gui.Views.Information, err = setViewFromDimensions("information", "information", false)
|
||||
if err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
gui.Views.Information.BgColor = gocui.ColorDefault
|
||||
gui.Views.Information.FgColor = gocui.ColorGreen
|
||||
gui.Views.Information.Frame = false
|
||||
gui.renderString(gui.Views.Information, INFO_SECTION_PADDING+informationStr)
|
||||
}
|
||||
if gui.State.OldInformation != informationStr {
|
||||
gui.setViewContent(gui.Views.Information, informationStr)
|
||||
gui.setViewContentSync(gui.Views.Information, informationStr)
|
||||
gui.State.OldInformation = informationStr
|
||||
}
|
||||
|
||||
@@ -291,7 +297,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
||||
return gui.resizeCurrentPopupPanel()
|
||||
}
|
||||
|
||||
func (gui *Gui) setHiddenView(viewName string) (*gocui.View, error) {
|
||||
func (gui *Gui) prepareView(viewName string) (*gocui.View, error) {
|
||||
// arbitrarily giving the view enough size so that we don't get an error, but
|
||||
// it's expected that the view will be given the correct size before being shown
|
||||
return gui.g.SetView(viewName, 0, 0, 10, 10, 0)
|
||||
@@ -317,11 +323,6 @@ func (gui *Gui) onInitialViewsCreationForRepo() error {
|
||||
}
|
||||
|
||||
func (gui *Gui) onInitialViewsCreation() error {
|
||||
// creating some views which are hidden at the start but we need to exist so that we can set an initial ordering
|
||||
if err := gui.createHiddenViews(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// now we order the views (in order of bottom first)
|
||||
layerOneViews := []*gocui.View{
|
||||
// first layer. Ordering within this layer does not matter because there are
|
||||
@@ -334,21 +335,22 @@ func (gui *Gui) onInitialViewsCreation() error {
|
||||
gui.Views.CommitFiles,
|
||||
gui.Views.Main,
|
||||
gui.Views.Secondary,
|
||||
gui.Views.Extras,
|
||||
|
||||
// bottom line
|
||||
gui.Views.Options,
|
||||
gui.Views.AppStatus,
|
||||
gui.Views.Information,
|
||||
gui.Views.Search,
|
||||
gui.Views.SearchPrefix,
|
||||
gui.Views.SearchPrefix, // this view takes up one character. Its only purpose is to show the slash when searching
|
||||
|
||||
// popups. Ordering within this layer does not matter because there should
|
||||
// only be one popup shown at a time
|
||||
gui.Views.CommitMessage,
|
||||
gui.Views.Credentials,
|
||||
gui.Views.Menu,
|
||||
gui.Views.Suggestions,
|
||||
gui.Views.Confirmation,
|
||||
gui.Views.Credentials,
|
||||
|
||||
// this guy will cover everything else when it appears
|
||||
gui.Views.Limit,
|
||||
@@ -375,6 +377,15 @@ func (gui *Gui) onInitialViewsCreation() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if !gui.Config.GetUserConfig().DisableStartupPopups {
|
||||
popupTasks := []func(chan struct{}) error{}
|
||||
storedPopupVersion := gui.Config.GetAppState().StartupPopupVersion
|
||||
if storedPopupVersion < StartupPopupVersion {
|
||||
popupTasks = append(popupTasks, gui.showIntroPopupMessage)
|
||||
}
|
||||
gui.showInitialPopups(popupTasks)
|
||||
}
|
||||
|
||||
if gui.showRecentRepos {
|
||||
if err := gui.handleCreateRecentReposMenu(); err != nil {
|
||||
return err
|
||||
@@ -388,58 +399,3 @@ func (gui *Gui) onInitialViewsCreation() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) createHiddenViews() error {
|
||||
// doesn't matter where this view starts because it will be hidden
|
||||
var err error
|
||||
if gui.Views.CommitMessage, err = gui.setHiddenView("commitMessage"); err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
gui.Views.CommitMessage.Visible = false
|
||||
gui.Views.CommitMessage.Title = gui.Tr.CommitMessage
|
||||
gui.Views.CommitMessage.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.CommitMessage.Editable = true
|
||||
gui.Views.CommitMessage.Editor = gocui.EditorFunc(gui.commitMessageEditor)
|
||||
}
|
||||
|
||||
// doesn't matter where this view starts because it will be hidden
|
||||
if gui.Views.Credentials, err = gui.setHiddenView("credentials"); err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
gui.Views.Credentials.Visible = false
|
||||
gui.Views.Credentials.Title = gui.Tr.CredentialsUsername
|
||||
gui.Views.Credentials.FgColor = theme.GocuiDefaultTextColor
|
||||
gui.Views.Credentials.Editable = true
|
||||
}
|
||||
|
||||
// not worrying about setting attributes because that will be done when the view is actually shown
|
||||
gui.Views.Confirmation, err = gui.setHiddenView("confirmation")
|
||||
if err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
gui.Views.Confirmation.Visible = false
|
||||
}
|
||||
|
||||
// not worrying about setting attributes because that will be done when the view is actually shown
|
||||
gui.Views.Suggestions, err = gui.setHiddenView("suggestions")
|
||||
if err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
gui.Views.Suggestions.Visible = false
|
||||
}
|
||||
|
||||
// not worrying about setting attributes because that will be done when the view is actually shown
|
||||
gui.Views.Menu, err = gui.setHiddenView("menu")
|
||||
if err != nil {
|
||||
if err.Error() != UNKNOWN_VIEW_ERROR_MSG {
|
||||
return err
|
||||
}
|
||||
gui.Views.Menu.Visible = false
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
52
pkg/gui/lbl/focus.go
Normal file
52
pkg/gui/lbl/focus.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package lbl
|
||||
|
||||
import "github.com/jesseduffield/lazygit/pkg/utils"
|
||||
|
||||
func calculateOrigin(currentOrigin int, bufferHeight int, firstLineIdx int, lastLineIdx int, selectedLineIdx int, mode selectMode) int {
|
||||
needToSeeIdx, wantToSeeIdx := getNeedAndWantLineIdx(firstLineIdx, lastLineIdx, selectedLineIdx, mode)
|
||||
|
||||
return calculateNewOriginWithNeededAndWantedIdx(currentOrigin, bufferHeight, needToSeeIdx, wantToSeeIdx)
|
||||
}
|
||||
|
||||
// we want to scroll our origin so that the index we need to see is in view
|
||||
// and the other index we want to see (e.g. the other side of a line range)
|
||||
// is in as close to being in view as possible.
|
||||
func calculateNewOriginWithNeededAndWantedIdx(currentOrigin int, bufferHeight int, needToSeeIdx int, wantToSeeIdx int) int {
|
||||
origin := currentOrigin
|
||||
if needToSeeIdx < currentOrigin {
|
||||
origin = needToSeeIdx
|
||||
} else if needToSeeIdx > currentOrigin+bufferHeight {
|
||||
origin = needToSeeIdx - bufferHeight
|
||||
}
|
||||
|
||||
bottom := origin + bufferHeight
|
||||
|
||||
if wantToSeeIdx < origin {
|
||||
requiredChange := origin - wantToSeeIdx
|
||||
allowedChange := bottom - needToSeeIdx
|
||||
return origin - utils.Min(requiredChange, allowedChange)
|
||||
} else if wantToSeeIdx > origin+bufferHeight {
|
||||
requiredChange := wantToSeeIdx - bottom
|
||||
allowedChange := needToSeeIdx - origin
|
||||
return origin + utils.Min(requiredChange, allowedChange)
|
||||
} else {
|
||||
return origin
|
||||
}
|
||||
}
|
||||
|
||||
func getNeedAndWantLineIdx(firstLineIdx int, lastLineIdx int, selectedLineIdx int, mode selectMode) (int, int) {
|
||||
switch mode {
|
||||
case LINE:
|
||||
return selectedLineIdx, selectedLineIdx
|
||||
case RANGE:
|
||||
if selectedLineIdx == firstLineIdx {
|
||||
return firstLineIdx, lastLineIdx
|
||||
} else {
|
||||
return lastLineIdx, firstLineIdx
|
||||
}
|
||||
case HUNK:
|
||||
return firstLineIdx, lastLineIdx
|
||||
default:
|
||||
panic("unknown mode")
|
||||
}
|
||||
}
|
||||
100
pkg/gui/lbl/focus_test.go
Normal file
100
pkg/gui/lbl/focus_test.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package lbl
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewOrigin(t *testing.T) {
|
||||
type scenario struct {
|
||||
name string
|
||||
origin int
|
||||
bufferHeight int
|
||||
firstLineIdx int
|
||||
lastLineIdx int
|
||||
selectedLineIdx int
|
||||
selectMode selectMode
|
||||
expected int
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
name: "selection above scroll window",
|
||||
origin: 50,
|
||||
bufferHeight: 100,
|
||||
firstLineIdx: 10,
|
||||
lastLineIdx: 10,
|
||||
selectedLineIdx: 10,
|
||||
selectMode: LINE,
|
||||
expected: 10,
|
||||
},
|
||||
{
|
||||
name: "selection below scroll window",
|
||||
origin: 0,
|
||||
bufferHeight: 100,
|
||||
firstLineIdx: 150,
|
||||
lastLineIdx: 150,
|
||||
selectedLineIdx: 150,
|
||||
selectMode: LINE,
|
||||
expected: 50,
|
||||
},
|
||||
{
|
||||
name: "selection within scroll window",
|
||||
origin: 0,
|
||||
bufferHeight: 100,
|
||||
firstLineIdx: 50,
|
||||
lastLineIdx: 50,
|
||||
selectedLineIdx: 50,
|
||||
selectMode: LINE,
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "range ending below scroll window with selection at end of range",
|
||||
origin: 0,
|
||||
bufferHeight: 100,
|
||||
firstLineIdx: 40,
|
||||
lastLineIdx: 150,
|
||||
selectedLineIdx: 150,
|
||||
selectMode: RANGE,
|
||||
expected: 50,
|
||||
},
|
||||
{
|
||||
name: "range ending below scroll window with selection at beginning of range",
|
||||
origin: 0,
|
||||
bufferHeight: 100,
|
||||
firstLineIdx: 40,
|
||||
lastLineIdx: 150,
|
||||
selectedLineIdx: 40,
|
||||
selectMode: RANGE,
|
||||
expected: 40,
|
||||
},
|
||||
{
|
||||
name: "range starting above scroll window with selection at beginning of range",
|
||||
origin: 50,
|
||||
bufferHeight: 100,
|
||||
firstLineIdx: 40,
|
||||
lastLineIdx: 150,
|
||||
selectedLineIdx: 40,
|
||||
selectMode: RANGE,
|
||||
expected: 40,
|
||||
},
|
||||
{
|
||||
name: "hunk extending beyond both bounds of scroll window",
|
||||
origin: 50,
|
||||
bufferHeight: 100,
|
||||
firstLineIdx: 40,
|
||||
lastLineIdx: 200,
|
||||
selectedLineIdx: 70,
|
||||
selectMode: HUNK,
|
||||
expected: 40,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
assert.EqualValues(t, s.expected, calculateOrigin(s.origin, s.bufferHeight, s.firstLineIdx, s.lastLineIdx, s.selectedLineIdx, s.selectMode))
|
||||
})
|
||||
}
|
||||
}
|
||||
197
pkg/gui/lbl/state.go
Normal file
197
pkg/gui/lbl/state.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package lbl
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type State struct {
|
||||
selectedLineIdx int
|
||||
rangeStartLineIdx int
|
||||
diff string
|
||||
patchParser *patch.PatchParser
|
||||
selectMode selectMode
|
||||
}
|
||||
|
||||
// these represent what select mode we're in
|
||||
type selectMode int
|
||||
|
||||
const (
|
||||
LINE selectMode = iota
|
||||
RANGE
|
||||
HUNK
|
||||
)
|
||||
|
||||
func NewState(diff string, selectedLineIdx int, oldState *State, log *logrus.Entry) *State {
|
||||
patchParser := patch.NewPatchParser(log, diff)
|
||||
|
||||
if len(patchParser.StageableLines) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
rangeStartLineIdx := 0
|
||||
if oldState != nil {
|
||||
rangeStartLineIdx = oldState.rangeStartLineIdx
|
||||
}
|
||||
|
||||
selectMode := LINE
|
||||
// if we have clicked from the outside to focus the main view we'll pass in a non-negative line index so that we can instantly select that line
|
||||
if selectedLineIdx >= 0 {
|
||||
selectMode = RANGE
|
||||
rangeStartLineIdx = selectedLineIdx
|
||||
} else if oldState != nil {
|
||||
// if we previously had a selectMode of RANGE, we want that to now be line again
|
||||
if oldState.selectMode == HUNK {
|
||||
selectMode = HUNK
|
||||
}
|
||||
selectedLineIdx = patchParser.GetNextStageableLineIndex(oldState.selectedLineIdx)
|
||||
} else {
|
||||
selectedLineIdx = patchParser.StageableLines[0]
|
||||
}
|
||||
|
||||
return &State{
|
||||
patchParser: patchParser,
|
||||
selectedLineIdx: selectedLineIdx,
|
||||
selectMode: selectMode,
|
||||
rangeStartLineIdx: rangeStartLineIdx,
|
||||
diff: diff,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) GetSelectedLineIdx() int {
|
||||
return s.selectedLineIdx
|
||||
}
|
||||
|
||||
func (s *State) GetDiff() string {
|
||||
return s.diff
|
||||
}
|
||||
|
||||
func (s *State) ToggleSelectHunk() {
|
||||
if s.selectMode == HUNK {
|
||||
s.selectMode = LINE
|
||||
} else {
|
||||
s.selectMode = HUNK
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) ToggleSelectRange() {
|
||||
if s.selectMode == RANGE {
|
||||
s.selectMode = LINE
|
||||
} else {
|
||||
s.selectMode = RANGE
|
||||
s.rangeStartLineIdx = s.selectedLineIdx
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) SelectingHunk() bool {
|
||||
return s.selectMode == HUNK
|
||||
}
|
||||
|
||||
func (s *State) SelectingRange() bool {
|
||||
return s.selectMode == RANGE
|
||||
}
|
||||
|
||||
func (s *State) SelectingLine() bool {
|
||||
return s.selectMode == LINE
|
||||
}
|
||||
|
||||
func (s *State) SetLineSelectMode() {
|
||||
s.selectMode = LINE
|
||||
}
|
||||
|
||||
func (s *State) SelectLine(newSelectedLineIdx int) {
|
||||
if newSelectedLineIdx < 0 {
|
||||
newSelectedLineIdx = 0
|
||||
} else if newSelectedLineIdx > len(s.patchParser.PatchLines)-1 {
|
||||
newSelectedLineIdx = len(s.patchParser.PatchLines) - 1
|
||||
}
|
||||
|
||||
s.selectedLineIdx = newSelectedLineIdx
|
||||
}
|
||||
|
||||
func (s *State) SelectNewLineForRange(newSelectedLineIdx int) {
|
||||
s.rangeStartLineIdx = newSelectedLineIdx
|
||||
|
||||
s.selectMode = RANGE
|
||||
|
||||
s.SelectLine(newSelectedLineIdx)
|
||||
}
|
||||
|
||||
func (s *State) CycleSelection(forward bool) {
|
||||
if s.SelectingHunk() {
|
||||
s.CycleHunk(forward)
|
||||
} else {
|
||||
s.CycleLine(forward)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) CycleHunk(forward bool) {
|
||||
change := 1
|
||||
if !forward {
|
||||
change = -1
|
||||
}
|
||||
|
||||
newHunk := s.patchParser.GetHunkContainingLine(s.selectedLineIdx, change)
|
||||
s.selectedLineIdx = s.patchParser.GetNextStageableLineIndex(newHunk.FirstLineIdx)
|
||||
}
|
||||
|
||||
func (s *State) CycleLine(forward bool) {
|
||||
change := 1
|
||||
if !forward {
|
||||
change = -1
|
||||
}
|
||||
|
||||
s.SelectLine(s.selectedLineIdx + change)
|
||||
}
|
||||
|
||||
func (s *State) CurrentHunk() *patch.PatchHunk {
|
||||
return s.patchParser.GetHunkContainingLine(s.selectedLineIdx, 0)
|
||||
}
|
||||
|
||||
func (s *State) SelectedRange() (int, int) {
|
||||
switch s.selectMode {
|
||||
case HUNK:
|
||||
hunk := s.CurrentHunk()
|
||||
return hunk.FirstLineIdx, hunk.LastLineIdx()
|
||||
case RANGE:
|
||||
if s.rangeStartLineIdx > s.selectedLineIdx {
|
||||
return s.selectedLineIdx, s.rangeStartLineIdx
|
||||
} else {
|
||||
return s.rangeStartLineIdx, s.selectedLineIdx
|
||||
}
|
||||
case LINE:
|
||||
return s.selectedLineIdx, s.selectedLineIdx
|
||||
default:
|
||||
// should never happen
|
||||
return 0, 0
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) CurrentLineNumber() int {
|
||||
return s.CurrentHunk().LineNumberOfLine(s.selectedLineIdx)
|
||||
}
|
||||
|
||||
func (s *State) AdjustSelectedLineIdx(change int) {
|
||||
s.SelectLine(s.selectedLineIdx + change)
|
||||
}
|
||||
|
||||
func (s *State) RenderForLineIndices(includedLineIndices []int) string {
|
||||
firstLineIdx, lastLineIdx := s.SelectedRange()
|
||||
return s.patchParser.Render(firstLineIdx, lastLineIdx, includedLineIndices)
|
||||
}
|
||||
|
||||
func (s *State) SelectBottom() {
|
||||
s.SetLineSelectMode()
|
||||
s.SelectLine(len(s.patchParser.PatchLines) - 1)
|
||||
}
|
||||
|
||||
func (s *State) SelectTop() {
|
||||
s.SetLineSelectMode()
|
||||
s.SelectLine(0)
|
||||
}
|
||||
|
||||
func (s *State) CalculateOrigin(currentOrigin int, bufferHeight int) int {
|
||||
firstLineIdx, lastLineIdx := s.SelectedRange()
|
||||
|
||||
return calculateOrigin(currentOrigin, bufferHeight, firstLineIdx, lastLineIdx, s.GetSelectedLineIdx(), s.selectMode)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/lbl"
|
||||
)
|
||||
|
||||
// Currently there are two 'pseudo-panels' that make use of this 'pseudo-panel'.
|
||||
@@ -15,193 +16,105 @@ import (
|
||||
// staging_panel.go and patch_building_panel.go have functions specific to their
|
||||
// use cases
|
||||
|
||||
// these represent what select mode we're in
|
||||
type SelectMode int
|
||||
|
||||
const (
|
||||
LINE SelectMode = iota
|
||||
RANGE
|
||||
HUNK
|
||||
)
|
||||
|
||||
// returns whether the patch is empty so caller can escape if necessary
|
||||
// both diffs should be non-coloured because we'll parse them and colour them here
|
||||
func (gui *Gui) refreshLineByLinePanel(diff string, secondaryDiff string, secondaryFocused bool, selectedLineIdx int, state *lBlPanelState) (bool, error) {
|
||||
func (gui *Gui) refreshLineByLinePanel(diff string, secondaryDiff string, secondaryFocused bool, selectedLineIdx int) (bool, error) {
|
||||
gui.splitMainPanel(true)
|
||||
|
||||
patchParser, err := patch.NewPatchParser(gui.Log, diff)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
var oldState *lbl.State
|
||||
if gui.State.Panels.LineByLine != nil {
|
||||
oldState = gui.State.Panels.LineByLine.State
|
||||
}
|
||||
|
||||
if len(patchParser.StageableLines) == 0 {
|
||||
state := lbl.NewState(diff, selectedLineIdx, oldState, gui.Log)
|
||||
if state == nil {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var firstLineIdx int
|
||||
var lastLineIdx int
|
||||
selectMode := LINE
|
||||
// if we have clicked from the outside to focus the main view we'll pass in a non-negative line index so that we can instantly select that line
|
||||
if selectedLineIdx >= 0 {
|
||||
selectMode = RANGE
|
||||
firstLineIdx, lastLineIdx = selectedLineIdx, selectedLineIdx
|
||||
} else if state != nil {
|
||||
if state.SelectMode == HUNK {
|
||||
// this is tricky: we need to find out which hunk we just staged based on our old `state.PatchParser` (as opposed to the new `patchParser`)
|
||||
// we do this by getting the first line index of the original hunk, then
|
||||
// finding the next stageable line, then getting its containing hunk
|
||||
// in the new diff
|
||||
selectMode = HUNK
|
||||
prevNewHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0)
|
||||
selectedLineIdx = patchParser.GetNextStageableLineIndex(prevNewHunk.FirstLineIdx)
|
||||
newHunk := patchParser.GetHunkContainingLine(selectedLineIdx, 0)
|
||||
firstLineIdx, lastLineIdx = newHunk.FirstLineIdx, newHunk.LastLineIdx()
|
||||
} else {
|
||||
selectedLineIdx = patchParser.GetNextStageableLineIndex(state.SelectedLineIdx)
|
||||
firstLineIdx, lastLineIdx = selectedLineIdx, selectedLineIdx
|
||||
}
|
||||
} else {
|
||||
selectedLineIdx = patchParser.StageableLines[0]
|
||||
firstLineIdx, lastLineIdx = selectedLineIdx, selectedLineIdx
|
||||
}
|
||||
|
||||
state = &lBlPanelState{
|
||||
PatchParser: patchParser,
|
||||
SelectedLineIdx: selectedLineIdx,
|
||||
SelectMode: selectMode,
|
||||
FirstLineIdx: firstLineIdx,
|
||||
LastLineIdx: lastLineIdx,
|
||||
Diff: diff,
|
||||
gui.State.Panels.LineByLine = &LblPanelState{
|
||||
State: state,
|
||||
SecondaryFocused: secondaryFocused,
|
||||
}
|
||||
gui.State.Panels.LineByLine = state
|
||||
|
||||
if err := gui.refreshMainViewForLineByLine(state); err != nil {
|
||||
if err := gui.refreshMainViewForLineByLine(gui.State.Panels.LineByLine); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := gui.focusSelection(selectMode == HUNK, state); err != nil {
|
||||
if err := gui.focusSelection(gui.State.Panels.LineByLine); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
gui.Views.Secondary.Highlight = true
|
||||
gui.Views.Secondary.Wrap = false
|
||||
|
||||
secondaryPatchParser, err := patch.NewPatchParser(gui.Log, secondaryDiff)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
secondaryPatchParser := patch.NewPatchParser(gui.Log, secondaryDiff)
|
||||
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
gui.setViewContent(gui.Views.Secondary, secondaryPatchParser.Render(-1, -1, nil))
|
||||
return nil
|
||||
})
|
||||
gui.setViewContent(gui.Views.Secondary, secondaryPatchParser.Render(-1, -1, nil))
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSelectPrevLine() error {
|
||||
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
|
||||
return gui.LBLCycleLine(-1, state)
|
||||
return gui.withLBLActiveCheck(func(state *LblPanelState) error {
|
||||
state.CycleSelection(false)
|
||||
|
||||
return gui.refreshAndFocusLblPanel(state)
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSelectNextLine() error {
|
||||
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
|
||||
return gui.LBLCycleLine(+1, state)
|
||||
return gui.withLBLActiveCheck(func(state *LblPanelState) error {
|
||||
state.CycleSelection(true)
|
||||
|
||||
return gui.refreshAndFocusLblPanel(state)
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSelectPrevHunk() error {
|
||||
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
|
||||
newHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, -1)
|
||||
return gui.withLBLActiveCheck(func(state *LblPanelState) error {
|
||||
state.CycleHunk(false)
|
||||
|
||||
return gui.selectNewHunk(newHunk, state)
|
||||
return gui.refreshAndFocusLblPanel(state)
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleSelectNextHunk() error {
|
||||
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
|
||||
newHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 1)
|
||||
return gui.withLBLActiveCheck(func(state *LblPanelState) error {
|
||||
state.CycleHunk(true)
|
||||
|
||||
return gui.selectNewHunk(newHunk, state)
|
||||
return gui.refreshAndFocusLblPanel(state)
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) selectNewHunk(newHunk *patch.PatchHunk, state *lBlPanelState) error {
|
||||
state.SelectedLineIdx = state.PatchParser.GetNextStageableLineIndex(newHunk.FirstLineIdx)
|
||||
if state.SelectMode == HUNK {
|
||||
state.FirstLineIdx, state.LastLineIdx = newHunk.FirstLineIdx, newHunk.LastLineIdx()
|
||||
} else {
|
||||
state.FirstLineIdx, state.LastLineIdx = state.SelectedLineIdx, state.SelectedLineIdx
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshAndFocusLblPanel(state *LblPanelState) error {
|
||||
if err := gui.refreshMainViewForLineByLine(state); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.focusSelection(true, state)
|
||||
}
|
||||
|
||||
func (gui *Gui) LBLCycleLine(change int, state *lBlPanelState) error {
|
||||
if state.SelectMode == HUNK {
|
||||
newHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, change)
|
||||
return gui.selectNewHunk(newHunk, state)
|
||||
}
|
||||
|
||||
return gui.LBLSelectLine(state.SelectedLineIdx+change, state)
|
||||
}
|
||||
|
||||
func (gui *Gui) LBLSelectLine(newSelectedLineIdx int, state *lBlPanelState) error {
|
||||
if newSelectedLineIdx < 0 {
|
||||
newSelectedLineIdx = 0
|
||||
} else if newSelectedLineIdx > len(state.PatchParser.PatchLines)-1 {
|
||||
newSelectedLineIdx = len(state.PatchParser.PatchLines) - 1
|
||||
}
|
||||
|
||||
state.SelectedLineIdx = newSelectedLineIdx
|
||||
|
||||
if state.SelectMode == RANGE {
|
||||
if state.SelectedLineIdx < state.FirstLineIdx {
|
||||
state.FirstLineIdx = state.SelectedLineIdx
|
||||
} else {
|
||||
state.LastLineIdx = state.SelectedLineIdx
|
||||
}
|
||||
} else {
|
||||
state.LastLineIdx = state.SelectedLineIdx
|
||||
state.FirstLineIdx = state.SelectedLineIdx
|
||||
}
|
||||
|
||||
if err := gui.refreshMainViewForLineByLine(state); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.focusSelection(false, state)
|
||||
return gui.focusSelection(state)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleLBLMouseDown() error {
|
||||
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
|
||||
return gui.withLBLActiveCheck(func(state *LblPanelState) error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
newSelectedLineIdx := gui.Views.Main.SelectedLineIdx()
|
||||
state.FirstLineIdx = newSelectedLineIdx
|
||||
state.LastLineIdx = newSelectedLineIdx
|
||||
state.SelectNewLineForRange(gui.Views.Main.SelectedLineIdx())
|
||||
|
||||
state.SelectMode = RANGE
|
||||
|
||||
return gui.LBLSelectLine(newSelectedLineIdx, state)
|
||||
return gui.refreshAndFocusLblPanel(state)
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleMouseDrag() error {
|
||||
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
|
||||
return gui.withLBLActiveCheck(func(state *LblPanelState) error {
|
||||
if gui.popupPanelFocused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return gui.LBLSelectLine(gui.Views.Main.SelectedLineIdx(), state)
|
||||
state.SelectLine(gui.Views.Main.SelectedLineIdx())
|
||||
|
||||
return gui.refreshAndFocusLblPanel(state)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -211,7 +124,7 @@ func (gui *Gui) getSelectedCommitFileName() string {
|
||||
return gui.State.CommitFileManager.GetItemAtIndex(idx).GetPath()
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshMainViewForLineByLine(state *lBlPanelState) error {
|
||||
func (gui *Gui) refreshMainViewForLineByLine(state *LblPanelState) error {
|
||||
var includedLineIndices []int
|
||||
// I'd prefer not to have knowledge of contexts using this file but I'm not sure
|
||||
// how to get around this
|
||||
@@ -223,88 +136,53 @@ func (gui *Gui) refreshMainViewForLineByLine(state *lBlPanelState) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
colorDiff := state.PatchParser.Render(state.FirstLineIdx, state.LastLineIdx, includedLineIndices)
|
||||
colorDiff := state.RenderForLineIndices(includedLineIndices)
|
||||
|
||||
gui.Views.Main.Highlight = true
|
||||
gui.Views.Main.Wrap = false
|
||||
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
gui.setViewContent(gui.Views.Main, colorDiff)
|
||||
return nil
|
||||
})
|
||||
gui.setViewContent(gui.Views.Main, colorDiff)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// focusSelection works out the best focus for the staging panel given the
|
||||
// selected line and size of the hunk
|
||||
func (gui *Gui) focusSelection(includeCurrentHunk bool, state *lBlPanelState) error {
|
||||
func (gui *Gui) focusSelection(state *LblPanelState) error {
|
||||
stagingView := gui.Views.Main
|
||||
|
||||
_, viewHeight := stagingView.Size()
|
||||
bufferHeight := viewHeight - 1
|
||||
_, origin := stagingView.Origin()
|
||||
|
||||
firstLineIdx := state.SelectedLineIdx
|
||||
lastLineIdx := state.SelectedLineIdx
|
||||
selectedLineIdx := state.GetSelectedLineIdx()
|
||||
|
||||
if includeCurrentHunk {
|
||||
hunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0)
|
||||
firstLineIdx = hunk.FirstLineIdx
|
||||
lastLineIdx = hunk.LastLineIdx()
|
||||
}
|
||||
|
||||
margin := 0 // we may want to have a margin in place to show context but right now I'm thinking we keep this at zero
|
||||
|
||||
var newOrigin int
|
||||
if firstLineIdx-origin < margin {
|
||||
newOrigin = firstLineIdx - margin
|
||||
} else if lastLineIdx-origin > bufferHeight-margin {
|
||||
newOrigin = lastLineIdx - bufferHeight + margin
|
||||
} else {
|
||||
newOrigin = origin
|
||||
}
|
||||
newOrigin := state.CalculateOrigin(origin, bufferHeight)
|
||||
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
if err := stagingView.SetOrigin(0, newOrigin); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return stagingView.SetCursor(0, state.SelectedLineIdx-newOrigin)
|
||||
return stagingView.SetCursor(0, selectedLineIdx-newOrigin)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleToggleSelectRange() error {
|
||||
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
|
||||
if state.SelectMode == RANGE {
|
||||
state.SelectMode = LINE
|
||||
} else {
|
||||
state.SelectMode = RANGE
|
||||
}
|
||||
state.FirstLineIdx, state.LastLineIdx = state.SelectedLineIdx, state.SelectedLineIdx
|
||||
return gui.withLBLActiveCheck(func(state *LblPanelState) error {
|
||||
state.ToggleSelectRange()
|
||||
|
||||
return gui.refreshMainViewForLineByLine(state)
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleToggleSelectHunk() error {
|
||||
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
|
||||
if state.SelectMode == HUNK {
|
||||
state.SelectMode = LINE
|
||||
state.FirstLineIdx, state.LastLineIdx = state.SelectedLineIdx, state.SelectedLineIdx
|
||||
} else {
|
||||
state.SelectMode = HUNK
|
||||
selectedHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0)
|
||||
state.FirstLineIdx, state.LastLineIdx = selectedHunk.FirstLineIdx, selectedHunk.LastLineIdx()
|
||||
}
|
||||
return gui.withLBLActiveCheck(func(state *LblPanelState) error {
|
||||
state.ToggleSelectHunk()
|
||||
|
||||
if err := gui.refreshMainViewForLineByLine(state); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.focusSelection(state.SelectMode == HUNK, state)
|
||||
return gui.refreshAndFocusLblPanel(state)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -313,7 +191,7 @@ func (gui *Gui) escapeLineByLinePanel() {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleOpenFileAtLine() error {
|
||||
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
|
||||
return gui.withLBLActiveCheck(func(state *LblPanelState) error {
|
||||
// again, would be good to use inheritance here (or maybe even composition)
|
||||
var filename string
|
||||
switch gui.State.MainContext {
|
||||
@@ -330,8 +208,7 @@ func (gui *Gui) handleOpenFileAtLine() error {
|
||||
}
|
||||
|
||||
// need to look at current index, then work out what my hunk's header information is, and see how far my line is away from the hunk header
|
||||
selectedHunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0)
|
||||
lineNumber := selectedHunk.LineNumberOfLine(state.SelectedLineIdx)
|
||||
lineNumber := state.CurrentLineNumber()
|
||||
filenameWithLineNum := fmt.Sprintf("%s:%d", filename, lineNumber)
|
||||
if err := gui.OSCommand.OpenFile(filenameWithLineNum); err != nil {
|
||||
return err
|
||||
@@ -342,48 +219,49 @@ func (gui *Gui) handleOpenFileAtLine() error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleLineByLineNextPage() error {
|
||||
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
|
||||
newSelectedLineIdx := state.SelectedLineIdx + gui.pageDelta(gui.Views.Main)
|
||||
return gui.withLBLActiveCheck(func(state *LblPanelState) error {
|
||||
state.SetLineSelectMode()
|
||||
state.AdjustSelectedLineIdx(gui.pageDelta(gui.Views.Main))
|
||||
|
||||
return gui.lineByLineNavigateTo(newSelectedLineIdx, state)
|
||||
return gui.refreshAndFocusLblPanel(state)
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleLineByLinePrevPage() error {
|
||||
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
|
||||
newSelectedLineIdx := state.SelectedLineIdx - gui.pageDelta(gui.Views.Main)
|
||||
return gui.withLBLActiveCheck(func(state *LblPanelState) error {
|
||||
state.SetLineSelectMode()
|
||||
state.AdjustSelectedLineIdx(-gui.pageDelta(gui.Views.Main))
|
||||
|
||||
return gui.lineByLineNavigateTo(newSelectedLineIdx, state)
|
||||
return gui.refreshAndFocusLblPanel(state)
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleLineByLineGotoBottom() error {
|
||||
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
|
||||
newSelectedLineIdx := len(state.PatchParser.PatchLines) - 1
|
||||
return gui.withLBLActiveCheck(func(state *LblPanelState) error {
|
||||
state.SelectBottom()
|
||||
|
||||
return gui.lineByLineNavigateTo(newSelectedLineIdx, state)
|
||||
return gui.refreshAndFocusLblPanel(state)
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleLineByLineGotoTop() error {
|
||||
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
|
||||
return gui.lineByLineNavigateTo(0, state)
|
||||
return gui.withLBLActiveCheck(func(state *LblPanelState) error {
|
||||
state.SelectTop()
|
||||
|
||||
return gui.refreshAndFocusLblPanel(state)
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handlelineByLineNavigateTo(selectedLineIdx int) error {
|
||||
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
|
||||
return gui.lineByLineNavigateTo(selectedLineIdx, state)
|
||||
return gui.withLBLActiveCheck(func(state *LblPanelState) error {
|
||||
state.SetLineSelectMode()
|
||||
state.SelectLine(selectedLineIdx)
|
||||
|
||||
return gui.refreshAndFocusLblPanel(state)
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) lineByLineNavigateTo(selectedLineIdx int, state *lBlPanelState) error {
|
||||
state.SelectMode = LINE
|
||||
|
||||
return gui.LBLSelectLine(selectedLineIdx, state)
|
||||
}
|
||||
|
||||
func (gui *Gui) withLBLActiveCheck(f func(*lBlPanelState) error) error {
|
||||
func (gui *Gui) withLBLActiveCheck(f func(*LblPanelState) error) error {
|
||||
gui.Mutexes.LineByLinePanelMutex.Lock()
|
||||
defer gui.Mutexes.LineByLinePanelMutex.Unlock()
|
||||
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type ListContext struct {
|
||||
ViewName string
|
||||
ContextKey ContextKey
|
||||
GetItemsLength func() int
|
||||
GetDisplayStrings func() [][]string
|
||||
OnFocus func() error
|
||||
OnFocusLost func() error
|
||||
OnClickSelectedItem func() error
|
||||
OnGetOptionsMap func() map[string]string
|
||||
|
||||
// the boolean here tells us whether the item is nil. This is needed because you can't work it out on the calling end once the pointer is wrapped in an interface (unless you want to use reflection)
|
||||
SelectedItem func() (ListItem, bool)
|
||||
@@ -23,11 +13,8 @@ type ListContext struct {
|
||||
|
||||
Gui *Gui
|
||||
ResetMainViewOriginOnFocus bool
|
||||
Kind ContextKind
|
||||
ParentContext Context
|
||||
// we can't know on the calling end whether a Context is actually a nil value without reflection, so we're storing this flag here to tell us. There has got to be a better way around this.
|
||||
hasParent bool
|
||||
WindowName string
|
||||
|
||||
*BasicContext
|
||||
}
|
||||
|
||||
type IListPanelState interface {
|
||||
@@ -47,30 +34,6 @@ func (lc *ListContext) GetSelectedItem() (ListItem, bool) {
|
||||
return lc.SelectedItem()
|
||||
}
|
||||
|
||||
func (lc *ListContext) SetWindowName(windowName string) {
|
||||
lc.WindowName = windowName
|
||||
}
|
||||
|
||||
func (lc *ListContext) GetWindowName() string {
|
||||
windowName := lc.WindowName
|
||||
|
||||
if windowName != "" {
|
||||
return windowName
|
||||
}
|
||||
|
||||
// TODO: actually set this for everything so we don't default to the view name
|
||||
return lc.ViewName
|
||||
}
|
||||
|
||||
func (lc *ListContext) SetParentContext(c Context) {
|
||||
lc.ParentContext = c
|
||||
lc.hasParent = true
|
||||
}
|
||||
|
||||
func (lc *ListContext) GetParentContext() (Context, bool) {
|
||||
return lc.ParentContext, lc.hasParent
|
||||
}
|
||||
|
||||
func (lc *ListContext) GetSelectedItemId() string {
|
||||
item, ok := lc.SelectedItem()
|
||||
|
||||
@@ -81,13 +44,6 @@ func (lc *ListContext) GetSelectedItemId() string {
|
||||
return item.ID()
|
||||
}
|
||||
|
||||
func (lc *ListContext) GetOptionsMap() map[string]string {
|
||||
if lc.OnGetOptionsMap != nil {
|
||||
return lc.OnGetOptionsMap()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnFocus assumes that the content of the context has already been rendered to the view. OnRender is the function which actually renders the content to the view
|
||||
func (lc *ListContext) OnRender() error {
|
||||
view, err := lc.Gui.g.View(lc.ViewName)
|
||||
@@ -103,18 +59,6 @@ func (lc *ListContext) OnRender() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lc *ListContext) GetKey() ContextKey {
|
||||
return lc.ContextKey
|
||||
}
|
||||
|
||||
func (lc *ListContext) GetKind() ContextKind {
|
||||
return lc.Kind
|
||||
}
|
||||
|
||||
func (lc *ListContext) GetViewName() string {
|
||||
return lc.ViewName
|
||||
}
|
||||
|
||||
func (lc *ListContext) HandleFocusLost() error {
|
||||
if lc.OnFocusLost != nil {
|
||||
return lc.OnFocusLost()
|
||||
@@ -252,349 +196,3 @@ func (lc *ListContext) onSearchSelect(selectedLineIdx int) error {
|
||||
lc.GetPanelState().SetSelectedLineIdx(selectedLineIdx)
|
||||
return lc.HandleFocus()
|
||||
}
|
||||
|
||||
func (gui *Gui) menuListContext() *ListContext {
|
||||
return &ListContext{
|
||||
ViewName: "menu",
|
||||
ContextKey: "menu",
|
||||
GetItemsLength: func() int { return gui.Views.Menu.LinesHeight() },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Menu },
|
||||
OnFocus: gui.handleMenuSelect,
|
||||
OnClickSelectedItem: gui.onMenuPress,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: false,
|
||||
Kind: PERSISTENT_POPUP,
|
||||
OnGetOptionsMap: gui.getMenuOptions,
|
||||
|
||||
// no GetDisplayStrings field because we do a custom render on menu creation
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) filesListContext() *ListContext {
|
||||
return &ListContext{
|
||||
ViewName: "files",
|
||||
ContextKey: FILES_CONTEXT_KEY,
|
||||
GetItemsLength: func() int { return gui.State.FileManager.GetItemsLength() },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Files },
|
||||
OnFocus: gui.focusAndSelectFile,
|
||||
OnClickSelectedItem: gui.handleFilePress,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: false,
|
||||
Kind: SIDE_CONTEXT,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
lines := gui.State.FileManager.Render(gui.State.Modes.Diffing.Ref, gui.State.Submodules)
|
||||
mappedLines := make([][]string, len(lines))
|
||||
for i, line := range lines {
|
||||
mappedLines[i] = []string{line}
|
||||
}
|
||||
|
||||
return mappedLines
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedFileNode()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) branchesListContext() *ListContext {
|
||||
return &ListContext{
|
||||
ViewName: "branches",
|
||||
ContextKey: LOCAL_BRANCHES_CONTEXT_KEY,
|
||||
GetItemsLength: func() int { return len(gui.State.Branches) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Branches },
|
||||
OnFocus: gui.handleBranchSelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
Kind: SIDE_CONTEXT,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetBranchListDisplayStrings(gui.State.Branches, gui.State.ScreenMode != SCREEN_NORMAL, gui.State.Modes.Diffing.Ref)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedBranch()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) remotesListContext() *ListContext {
|
||||
return &ListContext{
|
||||
ViewName: "branches",
|
||||
ContextKey: REMOTES_CONTEXT_KEY,
|
||||
GetItemsLength: func() int { return len(gui.State.Remotes) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Remotes },
|
||||
OnFocus: gui.handleRemoteSelect,
|
||||
OnClickSelectedItem: gui.handleRemoteEnter,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
Kind: SIDE_CONTEXT,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetRemoteListDisplayStrings(gui.State.Remotes, gui.State.Modes.Diffing.Ref)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedRemote()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) remoteBranchesListContext() *ListContext {
|
||||
return &ListContext{
|
||||
ViewName: "branches",
|
||||
ContextKey: REMOTE_BRANCHES_CONTEXT_KEY,
|
||||
GetItemsLength: func() int { return len(gui.State.RemoteBranches) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.RemoteBranches },
|
||||
OnFocus: gui.handleRemoteBranchSelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
Kind: SIDE_CONTEXT,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetRemoteBranchListDisplayStrings(gui.State.RemoteBranches, gui.State.Modes.Diffing.Ref)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedRemoteBranch()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) tagsListContext() *ListContext {
|
||||
return &ListContext{
|
||||
ViewName: "branches",
|
||||
ContextKey: TAGS_CONTEXT_KEY,
|
||||
GetItemsLength: func() int { return len(gui.State.Tags) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Tags },
|
||||
OnFocus: gui.handleTagSelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
Kind: SIDE_CONTEXT,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetTagListDisplayStrings(gui.State.Tags, gui.State.Modes.Diffing.Ref)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedTag()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) branchCommitsListContext() *ListContext {
|
||||
return &ListContext{
|
||||
ViewName: "commits",
|
||||
ContextKey: BRANCH_COMMITS_CONTEXT_KEY,
|
||||
GetItemsLength: func() int { return len(gui.State.Commits) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Commits },
|
||||
OnFocus: gui.handleCommitSelect,
|
||||
OnClickSelectedItem: gui.handleViewCommitFiles,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
Kind: SIDE_CONTEXT,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetCommitListDisplayStrings(gui.State.Commits, gui.State.ScreenMode != SCREEN_NORMAL, gui.cherryPickedCommitShaMap(), gui.State.Modes.Diffing.Ref)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedLocalCommit()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) reflogCommitsListContext() *ListContext {
|
||||
return &ListContext{
|
||||
ViewName: "commits",
|
||||
ContextKey: REFLOG_COMMITS_CONTEXT_KEY,
|
||||
GetItemsLength: func() int { return len(gui.State.FilteredReflogCommits) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.ReflogCommits },
|
||||
OnFocus: gui.handleReflogCommitSelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
Kind: SIDE_CONTEXT,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetReflogCommitListDisplayStrings(gui.State.FilteredReflogCommits, gui.State.ScreenMode != SCREEN_NORMAL, gui.cherryPickedCommitShaMap(), gui.State.Modes.Diffing.Ref)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedReflogCommit()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) subCommitsListContext() *ListContext {
|
||||
return &ListContext{
|
||||
ViewName: "branches",
|
||||
ContextKey: SUB_COMMITS_CONTEXT_KEY,
|
||||
GetItemsLength: func() int { return len(gui.State.SubCommits) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.SubCommits },
|
||||
OnFocus: gui.handleSubCommitSelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
Kind: SIDE_CONTEXT,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetCommitListDisplayStrings(gui.State.SubCommits, gui.State.ScreenMode != SCREEN_NORMAL, gui.cherryPickedCommitShaMap(), gui.State.Modes.Diffing.Ref)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedSubCommit()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) stashListContext() *ListContext {
|
||||
return &ListContext{
|
||||
ViewName: "stash",
|
||||
ContextKey: STASH_CONTEXT_KEY,
|
||||
GetItemsLength: func() int { return len(gui.State.StashEntries) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Stash },
|
||||
OnFocus: gui.handleStashEntrySelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
Kind: SIDE_CONTEXT,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetStashEntryListDisplayStrings(gui.State.StashEntries, gui.State.Modes.Diffing.Ref)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedStashEntry()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) commitFilesListContext() *ListContext {
|
||||
return &ListContext{
|
||||
ViewName: "commitFiles",
|
||||
WindowName: "commits",
|
||||
ContextKey: COMMIT_FILES_CONTEXT_KEY,
|
||||
GetItemsLength: func() int { return gui.State.CommitFileManager.GetItemsLength() },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.CommitFiles },
|
||||
OnFocus: gui.handleCommitFileSelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
Kind: SIDE_CONTEXT,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
if gui.State.CommitFileManager.GetItemsLength() == 0 {
|
||||
return [][]string{{utils.ColoredString("(none)", color.FgRed)}}
|
||||
}
|
||||
|
||||
lines := gui.State.CommitFileManager.Render(gui.State.Modes.Diffing.Ref, gui.GitCommand.PatchManager)
|
||||
mappedLines := make([][]string, len(lines))
|
||||
for i, line := range lines {
|
||||
mappedLines[i] = []string{line}
|
||||
}
|
||||
|
||||
return mappedLines
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedCommitFileNode()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) submodulesListContext() *ListContext {
|
||||
return &ListContext{
|
||||
ViewName: "files",
|
||||
WindowName: "files",
|
||||
ContextKey: SUBMODULES_CONTEXT_KEY,
|
||||
GetItemsLength: func() int { return len(gui.State.Submodules) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Submodules },
|
||||
OnFocus: gui.handleSubmoduleSelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
Kind: SIDE_CONTEXT,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetSubmoduleListDisplayStrings(gui.State.Submodules)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedSubmodule()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) suggestionsListContext() *ListContext {
|
||||
return &ListContext{
|
||||
ViewName: "suggestions",
|
||||
WindowName: "suggestions",
|
||||
ContextKey: SUGGESTIONS_CONTEXT_KEY,
|
||||
GetItemsLength: func() int { return len(gui.State.Suggestions) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Suggestions },
|
||||
OnFocus: func() error { return nil },
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: false,
|
||||
Kind: PERSISTENT_POPUP,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetSuggestionListDisplayStrings(gui.State.Suggestions)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) getListContexts() []*ListContext {
|
||||
return []*ListContext{
|
||||
gui.State.Contexts.Menu,
|
||||
gui.State.Contexts.Files,
|
||||
gui.State.Contexts.Branches,
|
||||
gui.State.Contexts.Remotes,
|
||||
gui.State.Contexts.RemoteBranches,
|
||||
gui.State.Contexts.Tags,
|
||||
gui.State.Contexts.BranchCommits,
|
||||
gui.State.Contexts.BranchCommits,
|
||||
gui.State.Contexts.ReflogCommits,
|
||||
gui.State.Contexts.SubCommits,
|
||||
gui.State.Contexts.Stash,
|
||||
gui.State.Contexts.CommitFiles,
|
||||
gui.State.Contexts.Submodules,
|
||||
gui.State.Contexts.Suggestions,
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) getListContextKeyBindings() []*Binding {
|
||||
bindings := make([]*Binding, 0)
|
||||
|
||||
keybindingConfig := gui.Config.GetUserConfig().Keybinding
|
||||
|
||||
for _, listContext := range gui.getListContexts() {
|
||||
listContext := listContext
|
||||
|
||||
bindings = append(bindings, []*Binding{
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gui.getKey(keybindingConfig.Universal.PrevItemAlt), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gui.getKey(keybindingConfig.Universal.PrevItem), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gocui.MouseWheelUp, Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gui.getKey(keybindingConfig.Universal.NextItemAlt), Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gui.getKey(keybindingConfig.Universal.NextItem), Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gui.getKey(keybindingConfig.Universal.PrevPage), Modifier: gocui.ModNone, Handler: listContext.handlePrevPage, Description: gui.Tr.LcPrevPage},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gui.getKey(keybindingConfig.Universal.NextPage), Modifier: gocui.ModNone, Handler: listContext.handleNextPage, Description: gui.Tr.LcNextPage},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gui.getKey(keybindingConfig.Universal.GotoTop), Modifier: gocui.ModNone, Handler: listContext.handleGotoTop, Description: gui.Tr.LcGotoTop},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
|
||||
{ViewName: listContext.ViewName, Contexts: []string{string(listContext.ContextKey)}, Key: gocui.MouseLeft, Modifier: gocui.ModNone, Handler: listContext.handleClick},
|
||||
}...)
|
||||
|
||||
// the commits panel needs to lazyload things so it has a couple of its own handlers
|
||||
openSearchHandler := gui.handleOpenSearch
|
||||
gotoBottomHandler := listContext.handleGotoBottom
|
||||
if listContext.ViewName == "commits" {
|
||||
openSearchHandler = gui.handleOpenSearchForCommitsPanel
|
||||
gotoBottomHandler = gui.handleGotoBottomForCommitsPanel
|
||||
}
|
||||
|
||||
bindings = append(bindings, []*Binding{
|
||||
{
|
||||
ViewName: listContext.ViewName,
|
||||
Contexts: []string{string(listContext.ContextKey)},
|
||||
Key: gui.getKey(keybindingConfig.Universal.StartSearch),
|
||||
Handler: func() error { return openSearchHandler(listContext.ViewName) },
|
||||
Description: gui.Tr.LcStartSearch,
|
||||
Tag: "navigation",
|
||||
},
|
||||
{
|
||||
ViewName: listContext.ViewName,
|
||||
Contexts: []string{string(listContext.ContextKey)},
|
||||
Key: gui.getKey(keybindingConfig.Universal.GotoBottom),
|
||||
Handler: gotoBottomHandler,
|
||||
Description: gui.Tr.LcGotoBottom,
|
||||
Tag: "navigation",
|
||||
},
|
||||
}...)
|
||||
}
|
||||
|
||||
return bindings
|
||||
}
|
||||
|
||||
389
pkg/gui/list_context_config.go
Normal file
389
pkg/gui/list_context_config.go
Normal file
@@ -0,0 +1,389 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func (gui *Gui) menuListContext() *ListContext {
|
||||
return &ListContext{
|
||||
BasicContext: &BasicContext{
|
||||
ViewName: "menu",
|
||||
Key: "menu",
|
||||
Kind: PERSISTENT_POPUP,
|
||||
OnGetOptionsMap: gui.getMenuOptions,
|
||||
},
|
||||
GetItemsLength: func() int { return gui.Views.Menu.LinesHeight() },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Menu },
|
||||
OnFocus: gui.handleMenuSelect,
|
||||
OnClickSelectedItem: gui.onMenuPress,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: false,
|
||||
|
||||
// no GetDisplayStrings field because we do a custom render on menu creation
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) filesListContext() *ListContext {
|
||||
return &ListContext{
|
||||
BasicContext: &BasicContext{
|
||||
ViewName: "files",
|
||||
WindowName: "files",
|
||||
Key: FILES_CONTEXT_KEY,
|
||||
Kind: SIDE_CONTEXT,
|
||||
},
|
||||
GetItemsLength: func() int { return gui.State.FileManager.GetItemsLength() },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Files },
|
||||
OnFocus: gui.focusAndSelectFile,
|
||||
OnClickSelectedItem: gui.handleFilePress,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: false,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
lines := gui.State.FileManager.Render(gui.State.Modes.Diffing.Ref, gui.State.Submodules)
|
||||
mappedLines := make([][]string, len(lines))
|
||||
for i, line := range lines {
|
||||
mappedLines[i] = []string{line}
|
||||
}
|
||||
|
||||
return mappedLines
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedFileNode()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) branchesListContext() *ListContext {
|
||||
return &ListContext{
|
||||
BasicContext: &BasicContext{
|
||||
ViewName: "branches",
|
||||
WindowName: "branches",
|
||||
Key: LOCAL_BRANCHES_CONTEXT_KEY,
|
||||
Kind: SIDE_CONTEXT,
|
||||
},
|
||||
GetItemsLength: func() int { return len(gui.State.Branches) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Branches },
|
||||
OnFocus: gui.handleBranchSelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetBranchListDisplayStrings(gui.State.Branches, gui.State.ScreenMode != SCREEN_NORMAL, gui.State.Modes.Diffing.Ref)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedBranch()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) remotesListContext() *ListContext {
|
||||
return &ListContext{
|
||||
BasicContext: &BasicContext{
|
||||
ViewName: "branches",
|
||||
WindowName: "branches",
|
||||
Key: REMOTES_CONTEXT_KEY,
|
||||
Kind: SIDE_CONTEXT,
|
||||
},
|
||||
GetItemsLength: func() int { return len(gui.State.Remotes) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Remotes },
|
||||
OnFocus: gui.handleRemoteSelect,
|
||||
OnClickSelectedItem: gui.handleRemoteEnter,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetRemoteListDisplayStrings(gui.State.Remotes, gui.State.Modes.Diffing.Ref)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedRemote()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) remoteBranchesListContext() *ListContext {
|
||||
return &ListContext{
|
||||
BasicContext: &BasicContext{
|
||||
ViewName: "branches",
|
||||
WindowName: "branches",
|
||||
Key: REMOTE_BRANCHES_CONTEXT_KEY,
|
||||
Kind: SIDE_CONTEXT,
|
||||
},
|
||||
GetItemsLength: func() int { return len(gui.State.RemoteBranches) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.RemoteBranches },
|
||||
OnFocus: gui.handleRemoteBranchSelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetRemoteBranchListDisplayStrings(gui.State.RemoteBranches, gui.State.Modes.Diffing.Ref)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedRemoteBranch()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) tagsListContext() *ListContext {
|
||||
return &ListContext{
|
||||
BasicContext: &BasicContext{
|
||||
ViewName: "branches",
|
||||
WindowName: "branches",
|
||||
Key: TAGS_CONTEXT_KEY,
|
||||
Kind: SIDE_CONTEXT,
|
||||
},
|
||||
GetItemsLength: func() int { return len(gui.State.Tags) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Tags },
|
||||
OnFocus: gui.handleTagSelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetTagListDisplayStrings(gui.State.Tags, gui.State.Modes.Diffing.Ref)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedTag()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) branchCommitsListContext() *ListContext {
|
||||
return &ListContext{
|
||||
BasicContext: &BasicContext{
|
||||
ViewName: "commits",
|
||||
WindowName: "commits",
|
||||
Key: BRANCH_COMMITS_CONTEXT_KEY,
|
||||
Kind: SIDE_CONTEXT,
|
||||
},
|
||||
GetItemsLength: func() int { return len(gui.State.Commits) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Commits },
|
||||
OnFocus: gui.handleCommitSelect,
|
||||
OnClickSelectedItem: gui.handleViewCommitFiles,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetCommitListDisplayStrings(gui.State.Commits, gui.State.ScreenMode != SCREEN_NORMAL, gui.cherryPickedCommitShaMap(), gui.State.Modes.Diffing.Ref)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedLocalCommit()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) reflogCommitsListContext() *ListContext {
|
||||
return &ListContext{
|
||||
BasicContext: &BasicContext{
|
||||
ViewName: "commits",
|
||||
WindowName: "commits",
|
||||
Key: REFLOG_COMMITS_CONTEXT_KEY,
|
||||
Kind: SIDE_CONTEXT,
|
||||
},
|
||||
GetItemsLength: func() int { return len(gui.State.FilteredReflogCommits) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.ReflogCommits },
|
||||
OnFocus: gui.handleReflogCommitSelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetReflogCommitListDisplayStrings(gui.State.FilteredReflogCommits, gui.State.ScreenMode != SCREEN_NORMAL, gui.cherryPickedCommitShaMap(), gui.State.Modes.Diffing.Ref)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedReflogCommit()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) subCommitsListContext() *ListContext {
|
||||
return &ListContext{
|
||||
BasicContext: &BasicContext{
|
||||
ViewName: "branches",
|
||||
WindowName: "branches",
|
||||
Key: SUB_COMMITS_CONTEXT_KEY,
|
||||
Kind: SIDE_CONTEXT,
|
||||
},
|
||||
GetItemsLength: func() int { return len(gui.State.SubCommits) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.SubCommits },
|
||||
OnFocus: gui.handleSubCommitSelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetCommitListDisplayStrings(gui.State.SubCommits, gui.State.ScreenMode != SCREEN_NORMAL, gui.cherryPickedCommitShaMap(), gui.State.Modes.Diffing.Ref)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedSubCommit()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) stashListContext() *ListContext {
|
||||
return &ListContext{
|
||||
BasicContext: &BasicContext{
|
||||
ViewName: "stash",
|
||||
WindowName: "stash",
|
||||
Key: STASH_CONTEXT_KEY,
|
||||
Kind: SIDE_CONTEXT,
|
||||
},
|
||||
GetItemsLength: func() int { return len(gui.State.StashEntries) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Stash },
|
||||
OnFocus: gui.handleStashEntrySelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetStashEntryListDisplayStrings(gui.State.StashEntries, gui.State.Modes.Diffing.Ref)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedStashEntry()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) commitFilesListContext() *ListContext {
|
||||
return &ListContext{
|
||||
BasicContext: &BasicContext{
|
||||
ViewName: "commitFiles",
|
||||
WindowName: "commits",
|
||||
Key: COMMIT_FILES_CONTEXT_KEY,
|
||||
Kind: SIDE_CONTEXT,
|
||||
},
|
||||
GetItemsLength: func() int { return gui.State.CommitFileManager.GetItemsLength() },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.CommitFiles },
|
||||
OnFocus: gui.handleCommitFileSelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
if gui.State.CommitFileManager.GetItemsLength() == 0 {
|
||||
return [][]string{{utils.ColoredString("(none)", color.FgRed)}}
|
||||
}
|
||||
|
||||
lines := gui.State.CommitFileManager.Render(gui.State.Modes.Diffing.Ref, gui.GitCommand.PatchManager)
|
||||
mappedLines := make([][]string, len(lines))
|
||||
for i, line := range lines {
|
||||
mappedLines[i] = []string{line}
|
||||
}
|
||||
|
||||
return mappedLines
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedCommitFileNode()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) submodulesListContext() *ListContext {
|
||||
return &ListContext{
|
||||
BasicContext: &BasicContext{
|
||||
ViewName: "files",
|
||||
WindowName: "files",
|
||||
Key: SUBMODULES_CONTEXT_KEY,
|
||||
Kind: SIDE_CONTEXT,
|
||||
},
|
||||
GetItemsLength: func() int { return len(gui.State.Submodules) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Submodules },
|
||||
OnFocus: gui.handleSubmoduleSelect,
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: true,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetSubmoduleListDisplayStrings(gui.State.Submodules)
|
||||
},
|
||||
SelectedItem: func() (ListItem, bool) {
|
||||
item := gui.getSelectedSubmodule()
|
||||
return item, item != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) suggestionsListContext() *ListContext {
|
||||
return &ListContext{
|
||||
BasicContext: &BasicContext{
|
||||
ViewName: "suggestions",
|
||||
WindowName: "suggestions",
|
||||
Key: SUGGESTIONS_CONTEXT_KEY,
|
||||
Kind: PERSISTENT_POPUP,
|
||||
},
|
||||
GetItemsLength: func() int { return len(gui.State.Suggestions) },
|
||||
GetPanelState: func() IListPanelState { return gui.State.Panels.Suggestions },
|
||||
OnFocus: func() error { return nil },
|
||||
Gui: gui,
|
||||
ResetMainViewOriginOnFocus: false,
|
||||
GetDisplayStrings: func() [][]string {
|
||||
return presentation.GetSuggestionListDisplayStrings(gui.State.Suggestions)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) getListContexts() []*ListContext {
|
||||
return []*ListContext{
|
||||
gui.State.Contexts.Menu,
|
||||
gui.State.Contexts.Files,
|
||||
gui.State.Contexts.Branches,
|
||||
gui.State.Contexts.Remotes,
|
||||
gui.State.Contexts.RemoteBranches,
|
||||
gui.State.Contexts.Tags,
|
||||
gui.State.Contexts.BranchCommits,
|
||||
gui.State.Contexts.BranchCommits,
|
||||
gui.State.Contexts.ReflogCommits,
|
||||
gui.State.Contexts.SubCommits,
|
||||
gui.State.Contexts.Stash,
|
||||
gui.State.Contexts.CommitFiles,
|
||||
gui.State.Contexts.Submodules,
|
||||
gui.State.Contexts.Suggestions,
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) getListContextKeyBindings() []*Binding {
|
||||
bindings := make([]*Binding, 0)
|
||||
|
||||
keybindingConfig := gui.Config.GetUserConfig().Keybinding
|
||||
|
||||
for _, listContext := range gui.getListContexts() {
|
||||
listContext := listContext
|
||||
|
||||
bindings = append(bindings, []*Binding{
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gui.getKey(keybindingConfig.Universal.PrevItemAlt), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gui.getKey(keybindingConfig.Universal.PrevItem), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gocui.MouseWheelUp, Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gui.getKey(keybindingConfig.Universal.NextItemAlt), Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gui.getKey(keybindingConfig.Universal.NextItem), Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gui.getKey(keybindingConfig.Universal.PrevPage), Modifier: gocui.ModNone, Handler: listContext.handlePrevPage, Description: gui.Tr.LcPrevPage},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gui.getKey(keybindingConfig.Universal.NextPage), Modifier: gocui.ModNone, Handler: listContext.handleNextPage, Description: gui.Tr.LcNextPage},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gui.getKey(keybindingConfig.Universal.GotoTop), Modifier: gocui.ModNone, Handler: listContext.handleGotoTop, Description: gui.Tr.LcGotoTop},
|
||||
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
|
||||
{ViewName: listContext.ViewName, Contexts: []string{string(listContext.Key)}, Key: gocui.MouseLeft, Modifier: gocui.ModNone, Handler: listContext.handleClick},
|
||||
}...)
|
||||
|
||||
// the commits panel needs to lazyload things so it has a couple of its own handlers
|
||||
openSearchHandler := gui.handleOpenSearch
|
||||
gotoBottomHandler := listContext.handleGotoBottom
|
||||
if listContext.ViewName == "commits" {
|
||||
openSearchHandler = gui.handleOpenSearchForCommitsPanel
|
||||
gotoBottomHandler = gui.handleGotoBottomForCommitsPanel
|
||||
}
|
||||
|
||||
bindings = append(bindings, []*Binding{
|
||||
{
|
||||
ViewName: listContext.ViewName,
|
||||
Contexts: []string{string(listContext.Key)},
|
||||
Key: gui.getKey(keybindingConfig.Universal.StartSearch),
|
||||
Handler: func() error { return openSearchHandler(listContext.ViewName) },
|
||||
Description: gui.Tr.LcStartSearch,
|
||||
Tag: "navigation",
|
||||
},
|
||||
{
|
||||
ViewName: listContext.ViewName,
|
||||
Contexts: []string{string(listContext.Key)},
|
||||
Key: gui.getKey(keybindingConfig.Universal.GotoBottom),
|
||||
Handler: gotoBottomHandler,
|
||||
Description: gui.Tr.LcGotoBottom,
|
||||
Tag: "navigation",
|
||||
},
|
||||
}...)
|
||||
}
|
||||
|
||||
return bindings
|
||||
}
|
||||
@@ -94,9 +94,13 @@ func (gui *Gui) createMenu(title string, items []*menuItem, createMenuOptions cr
|
||||
|
||||
func (gui *Gui) onMenuPress() error {
|
||||
selectedLine := gui.State.Panels.Menu.SelectedLineIdx
|
||||
if err := gui.returnFromContext(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.State.MenuItems[selectedLine].onPress(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gui.returnFromContext()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,23 +3,21 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"os"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/golang-collections/collections/stack"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts"
|
||||
)
|
||||
|
||||
func (gui *Gui) handleSelectTop() error {
|
||||
return gui.withMergeConflictLock(func() error {
|
||||
gui.takeOverMergeConflictScrolling()
|
||||
gui.State.Panels.Merging.ConflictTop = true
|
||||
gui.State.Panels.Merging.SelectTopOption()
|
||||
return gui.refreshMergePanel()
|
||||
})
|
||||
}
|
||||
@@ -27,7 +25,7 @@ func (gui *Gui) handleSelectTop() error {
|
||||
func (gui *Gui) handleSelectBottom() error {
|
||||
return gui.withMergeConflictLock(func() error {
|
||||
gui.takeOverMergeConflictScrolling()
|
||||
gui.State.Panels.Merging.ConflictTop = false
|
||||
gui.State.Panels.Merging.SelectBottomOption()
|
||||
return gui.refreshMergePanel()
|
||||
})
|
||||
}
|
||||
@@ -35,10 +33,7 @@ func (gui *Gui) handleSelectBottom() error {
|
||||
func (gui *Gui) handleSelectNextConflict() error {
|
||||
return gui.withMergeConflictLock(func() error {
|
||||
gui.takeOverMergeConflictScrolling()
|
||||
if gui.State.Panels.Merging.ConflictIndex >= len(gui.State.Panels.Merging.Conflicts)-1 {
|
||||
return nil
|
||||
}
|
||||
gui.State.Panels.Merging.ConflictIndex++
|
||||
gui.State.Panels.Merging.SelectNextConflict()
|
||||
return gui.refreshMergePanel()
|
||||
})
|
||||
}
|
||||
@@ -46,36 +41,31 @@ func (gui *Gui) handleSelectNextConflict() error {
|
||||
func (gui *Gui) handleSelectPrevConflict() error {
|
||||
return gui.withMergeConflictLock(func() error {
|
||||
gui.takeOverMergeConflictScrolling()
|
||||
if gui.State.Panels.Merging.ConflictIndex <= 0 {
|
||||
return nil
|
||||
}
|
||||
gui.State.Panels.Merging.ConflictIndex--
|
||||
gui.State.Panels.Merging.SelectPrevConflict()
|
||||
return gui.refreshMergePanel()
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) pushFileSnapshot() error {
|
||||
gitFile := gui.getSelectedFile()
|
||||
if gitFile == nil {
|
||||
return nil
|
||||
}
|
||||
content, err := gui.GitCommand.CatFile(gitFile.Name)
|
||||
content, err := gui.catSelectedFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gui.State.Panels.Merging.EditHistory.Push(content)
|
||||
gui.State.Panels.Merging.PushFileSnapshot(content)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handlePopFileSnapshot() error {
|
||||
if gui.State.Panels.Merging.EditHistory.Len() == 0 {
|
||||
prevContent, ok := gui.State.Panels.Merging.PopFileSnapshot()
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
prevContent := gui.State.Panels.Merging.EditHistory.Pop().(string)
|
||||
|
||||
gitFile := gui.getSelectedFile()
|
||||
if gitFile == nil {
|
||||
return nil
|
||||
}
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry("Undoing last conflict resolution", "Undo merge conflict resolution", false))
|
||||
if err := ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -85,28 +75,18 @@ func (gui *Gui) handlePopFileSnapshot() error {
|
||||
|
||||
func (gui *Gui) handlePickHunk() error {
|
||||
return gui.withMergeConflictLock(func() error {
|
||||
conflict := gui.getCurrentConflict()
|
||||
if conflict == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.takeOverMergeConflictScrolling()
|
||||
|
||||
if err := gui.pushFileSnapshot(); err != nil {
|
||||
ok, err := gui.resolveConflict(gui.State.Panels.Merging.Selection())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
selection := mergeconflicts.BOTTOM
|
||||
if gui.State.Panels.Merging.ConflictTop {
|
||||
selection = mergeconflicts.TOP
|
||||
}
|
||||
err := gui.resolveConflict(*conflict, selection)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// if that was the last conflict, finish the merge for this file
|
||||
if len(gui.State.Panels.Merging.Conflicts) == 1 {
|
||||
if gui.State.Panels.Merging.IsFinalConflict() {
|
||||
if err := gui.handleCompleteMerge(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -117,55 +97,51 @@ func (gui *Gui) handlePickHunk() error {
|
||||
|
||||
func (gui *Gui) handlePickBothHunks() error {
|
||||
return gui.withMergeConflictLock(func() error {
|
||||
conflict := gui.getCurrentConflict()
|
||||
if conflict == nil {
|
||||
gui.takeOverMergeConflictScrolling()
|
||||
|
||||
ok, err := gui.resolveConflict(mergeconflicts.BOTH)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
gui.takeOverMergeConflictScrolling()
|
||||
|
||||
if err := gui.pushFileSnapshot(); err != nil {
|
||||
return err
|
||||
}
|
||||
err := gui.resolveConflict(*conflict, mergeconflicts.BOTH)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return gui.refreshMergePanel()
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) getCurrentConflict() *commands.Conflict {
|
||||
if len(gui.State.Panels.Merging.Conflicts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex]
|
||||
}
|
||||
|
||||
func (gui *Gui) resolveConflict(conflict commands.Conflict, selection mergeconflicts.Selection) error {
|
||||
func (gui *Gui) resolveConflict(selection mergeconflicts.Selection) (bool, error) {
|
||||
gitFile := gui.getSelectedFile()
|
||||
if gitFile == nil {
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
file, err := os.Open(gitFile.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
reader := bufio.NewReader(file)
|
||||
output := ""
|
||||
for i := 0; true; i++ {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if !mergeconflicts.IsIndexToDelete(i, conflict, selection) {
|
||||
output += line
|
||||
}
|
||||
ok, output, err := gui.State.Panels.Merging.ContentAfterConflictResolve(gitFile.Name, selection)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return ioutil.WriteFile(gitFile.Name, []byte(output), 0644)
|
||||
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err := gui.pushFileSnapshot(); err != nil {
|
||||
return false, gui.surfaceError(err)
|
||||
}
|
||||
|
||||
var logStr string
|
||||
switch selection {
|
||||
case mergeconflicts.TOP:
|
||||
logStr = "Picking top hunk"
|
||||
case mergeconflicts.BOTTOM:
|
||||
logStr = "Picking bottom hunk"
|
||||
case mergeconflicts.BOTH:
|
||||
logStr = "Picking both hunks"
|
||||
}
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(logStr, "Resolve merge conflict", false))
|
||||
return true, ioutil.WriteFile(gitFile.Name, []byte(output), 0644)
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshMergePanelWithLock() error {
|
||||
@@ -184,17 +160,14 @@ func (gui *Gui) refreshMergePanel() error {
|
||||
})
|
||||
}
|
||||
|
||||
panelState.Conflicts = mergeconflicts.FindConflicts(cat)
|
||||
panelState.SetConflictsFromCat(cat)
|
||||
|
||||
// handle potential fixes that the user made in their editor since we last refreshed
|
||||
if len(panelState.Conflicts) == 0 {
|
||||
if panelState.NoConflicts() {
|
||||
return gui.handleCompleteMerge()
|
||||
} else if panelState.ConflictIndex > len(panelState.Conflicts)-1 {
|
||||
panelState.ConflictIndex = len(panelState.Conflicts) - 1
|
||||
}
|
||||
|
||||
hasFocus := gui.currentViewName() == "main"
|
||||
content := mergeconflicts.ColoredConflictFile(cat, panelState.Conflicts, panelState.ConflictIndex, panelState.ConflictTop, hasFocus)
|
||||
content := mergeconflicts.ColoredConflictFile(cat, panelState.State, hasFocus)
|
||||
|
||||
if err := gui.scrollToConflict(); err != nil {
|
||||
return err
|
||||
@@ -233,22 +206,24 @@ func (gui *Gui) scrollToConflict() error {
|
||||
}
|
||||
|
||||
panelState := gui.State.Panels.Merging
|
||||
if len(panelState.Conflicts) == 0 {
|
||||
if panelState.NoConflicts() {
|
||||
return nil
|
||||
}
|
||||
|
||||
mergingView := gui.Views.Main
|
||||
conflict := panelState.Conflicts[panelState.ConflictIndex]
|
||||
ox, _ := mergingView.Origin()
|
||||
_, height := mergingView.Size()
|
||||
conflictMiddle := (conflict.End + conflict.Start) / 2
|
||||
newOriginY := int(math.Max(0, float64(conflictMiddle-(height/2))))
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
return mergingView.SetOrigin(ox, newOriginY)
|
||||
})
|
||||
gui.centerYPos(gui.Views.Main, panelState.GetConflictMiddle())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) centerYPos(view *gocui.View, y int) {
|
||||
ox, _ := view.Origin()
|
||||
_, height := view.Size()
|
||||
newOriginY := int(math.Max(0, float64(y-(height/2))))
|
||||
gui.g.Update(func(g *gocui.Gui) error {
|
||||
return view.SetOrigin(ox, newOriginY)
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) getMergingOptions() map[string]string {
|
||||
keybindingConfig := gui.Config.GetUserConfig().Keybinding
|
||||
|
||||
@@ -264,7 +239,7 @@ func (gui *Gui) getMergingOptions() map[string]string {
|
||||
func (gui *Gui) handleEscapeMerge() error {
|
||||
gui.takeOverMergeConflictScrolling()
|
||||
|
||||
gui.State.Panels.Merging.EditHistory = stack.New()
|
||||
gui.State.Panels.Merging.Reset()
|
||||
if err := gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -331,8 +306,8 @@ func (gui *Gui) canScrollMergePanel() bool {
|
||||
}
|
||||
|
||||
func (gui *Gui) withMergeConflictLock(f func() error) error {
|
||||
gui.State.Panels.Merging.ConflictsMutex.Lock()
|
||||
defer gui.State.Panels.Merging.ConflictsMutex.Unlock()
|
||||
gui.State.Panels.Merging.Lock()
|
||||
defer gui.State.Panels.Merging.Unlock()
|
||||
|
||||
return f()
|
||||
}
|
||||
|
||||
60
pkg/gui/mergeconflicts/find_conflicts.go
Normal file
60
pkg/gui/mergeconflicts/find_conflicts.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package mergeconflicts
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// LineType tells us whether a given line is a start/middle/end marker of a conflict,
|
||||
// or if it's not a marker at all
|
||||
type LineType int
|
||||
|
||||
const (
|
||||
START LineType = iota
|
||||
MIDDLE
|
||||
END
|
||||
NOT_A_MARKER
|
||||
)
|
||||
|
||||
func findConflicts(content string) []*mergeConflict {
|
||||
conflicts := make([]*mergeConflict, 0)
|
||||
|
||||
if content == "" {
|
||||
return conflicts
|
||||
}
|
||||
|
||||
var newConflict *mergeConflict
|
||||
for i, line := range utils.SplitLines(content) {
|
||||
switch determineLineType(line) {
|
||||
case START:
|
||||
newConflict = &mergeConflict{start: i}
|
||||
case MIDDLE:
|
||||
newConflict.middle = i
|
||||
case END:
|
||||
newConflict.end = i
|
||||
conflicts = append(conflicts, newConflict)
|
||||
// reset value to avoid any possible silent mutations in further iterations
|
||||
newConflict = nil
|
||||
default:
|
||||
// line isn't a merge conflict marker so we just continue
|
||||
}
|
||||
}
|
||||
|
||||
return conflicts
|
||||
}
|
||||
|
||||
func determineLineType(line string) LineType {
|
||||
trimmedLine := strings.TrimPrefix(line, "++")
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(trimmedLine, "<<<<<<< "):
|
||||
return START
|
||||
case trimmedLine == "=======":
|
||||
return MIDDLE
|
||||
case strings.HasPrefix(trimmedLine, ">>>>>>> "):
|
||||
return END
|
||||
default:
|
||||
return NOT_A_MARKER
|
||||
}
|
||||
}
|
||||
57
pkg/gui/mergeconflicts/find_conflicts_test.go
Normal file
57
pkg/gui/mergeconflicts/find_conflicts_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package mergeconflicts
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDetermineLineType(t *testing.T) {
|
||||
type scenario struct {
|
||||
line string
|
||||
expected LineType
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
line: "",
|
||||
expected: NOT_A_MARKER,
|
||||
},
|
||||
{
|
||||
line: "blah",
|
||||
expected: NOT_A_MARKER,
|
||||
},
|
||||
{
|
||||
line: "<<<<<<< HEAD",
|
||||
expected: START,
|
||||
},
|
||||
{
|
||||
line: "<<<<<<< HEAD:my_branch",
|
||||
expected: START,
|
||||
},
|
||||
{
|
||||
line: "<<<<<<< MERGE_HEAD:my_branch",
|
||||
expected: START,
|
||||
},
|
||||
{
|
||||
line: "<<<<<<< Updated upstream:my_branch",
|
||||
expected: START,
|
||||
},
|
||||
{
|
||||
line: "<<<<<<< ours:my_branch",
|
||||
expected: START,
|
||||
},
|
||||
{
|
||||
line: "=======",
|
||||
expected: MIDDLE,
|
||||
},
|
||||
{
|
||||
line: ">>>>>>> blah",
|
||||
expected: END,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
assert.EqualValues(t, s.expected, determineLineType(s.line))
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package mergeconflicts
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type Selection int
|
||||
|
||||
const (
|
||||
TOP Selection = iota
|
||||
BOTTOM
|
||||
BOTH
|
||||
)
|
||||
|
||||
func FindConflicts(content string) []commands.Conflict {
|
||||
conflicts := make([]commands.Conflict, 0)
|
||||
|
||||
if content == "" {
|
||||
return conflicts
|
||||
}
|
||||
|
||||
var newConflict commands.Conflict
|
||||
for i, line := range utils.SplitLines(content) {
|
||||
trimmedLine := strings.TrimPrefix(line, "++")
|
||||
switch trimmedLine {
|
||||
case "<<<<<<< HEAD", "<<<<<<< MERGE_HEAD", "<<<<<<< Updated upstream", "<<<<<<< ours":
|
||||
newConflict = commands.Conflict{Start: i}
|
||||
case "=======":
|
||||
newConflict.Middle = i
|
||||
default:
|
||||
if strings.HasPrefix(trimmedLine, ">>>>>>> ") {
|
||||
newConflict.End = i
|
||||
conflicts = append(conflicts, newConflict)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return conflicts
|
||||
}
|
||||
|
||||
func ColoredConflictFile(content string, conflicts []commands.Conflict, conflictIndex int, conflictTop, hasFocus bool) string {
|
||||
if len(conflicts) == 0 {
|
||||
return content
|
||||
}
|
||||
conflict, remainingConflicts := shiftConflict(conflicts)
|
||||
var outputBuffer bytes.Buffer
|
||||
for i, line := range utils.SplitLines(content) {
|
||||
colourAttr := theme.DefaultTextColor
|
||||
if i == conflict.Start || i == conflict.Middle || i == conflict.End {
|
||||
colourAttr = color.FgRed
|
||||
}
|
||||
colour := color.New(colourAttr)
|
||||
if hasFocus && conflictIndex < len(conflicts) && conflicts[conflictIndex] == conflict && shouldHighlightLine(i, conflict, conflictTop) {
|
||||
colour.Add(color.Bold)
|
||||
colour.Add(theme.SelectedRangeBgColor)
|
||||
}
|
||||
if i == conflict.End && len(remainingConflicts) > 0 {
|
||||
conflict, remainingConflicts = shiftConflict(remainingConflicts)
|
||||
}
|
||||
outputBuffer.WriteString(utils.ColoredStringDirect(line, colour) + "\n")
|
||||
}
|
||||
return outputBuffer.String()
|
||||
}
|
||||
|
||||
func IsIndexToDelete(i int, conflict commands.Conflict, selection Selection) bool {
|
||||
return i == conflict.Middle ||
|
||||
i == conflict.Start ||
|
||||
i == conflict.End ||
|
||||
selection != BOTH &&
|
||||
(selection == BOTTOM && i > conflict.Start && i < conflict.Middle) ||
|
||||
(selection == TOP && i > conflict.Middle && i < conflict.End)
|
||||
}
|
||||
|
||||
func shiftConflict(conflicts []commands.Conflict) (commands.Conflict, []commands.Conflict) {
|
||||
return conflicts[0], conflicts[1:]
|
||||
}
|
||||
|
||||
func shouldHighlightLine(index int, conflict commands.Conflict, top bool) bool {
|
||||
return (index >= conflict.Start && index <= conflict.Middle && top) || (index >= conflict.Middle && index <= conflict.End && !top)
|
||||
}
|
||||
41
pkg/gui/mergeconflicts/rendering.go
Normal file
41
pkg/gui/mergeconflicts/rendering.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package mergeconflicts
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
func ColoredConflictFile(content string, state *State, hasFocus bool) string {
|
||||
if len(state.conflicts) == 0 {
|
||||
return content
|
||||
}
|
||||
conflict, remainingConflicts := shiftConflict(state.conflicts)
|
||||
var outputBuffer bytes.Buffer
|
||||
for i, line := range utils.SplitLines(content) {
|
||||
colourAttr := theme.DefaultTextColor
|
||||
if i == conflict.start || i == conflict.middle || i == conflict.end {
|
||||
colourAttr = color.FgRed
|
||||
}
|
||||
colour := color.New(colourAttr)
|
||||
if hasFocus && state.conflictIndex < len(state.conflicts) && *state.conflicts[state.conflictIndex] == *conflict && shouldHighlightLine(i, conflict, state.conflictTop) {
|
||||
colour.Add(color.Bold)
|
||||
colour.Add(theme.SelectedRangeBgColor)
|
||||
}
|
||||
if i == conflict.end && len(remainingConflicts) > 0 {
|
||||
conflict, remainingConflicts = shiftConflict(remainingConflicts)
|
||||
}
|
||||
outputBuffer.WriteString(utils.ColoredStringDirect(line, colour) + "\n")
|
||||
}
|
||||
return outputBuffer.String()
|
||||
}
|
||||
|
||||
func shiftConflict(conflicts []*mergeConflict) (*mergeConflict, []*mergeConflict) {
|
||||
return conflicts[0], conflicts[1:]
|
||||
}
|
||||
|
||||
func shouldHighlightLine(index int, conflict *mergeConflict, top bool) bool {
|
||||
return (index >= conflict.start && index <= conflict.middle && top) || (index >= conflict.middle && index <= conflict.end && !top)
|
||||
}
|
||||
153
pkg/gui/mergeconflicts/state.go
Normal file
153
pkg/gui/mergeconflicts/state.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package mergeconflicts
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/golang-collections/collections/stack"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type Selection int
|
||||
|
||||
const (
|
||||
TOP Selection = iota
|
||||
BOTTOM
|
||||
BOTH
|
||||
)
|
||||
|
||||
// mergeConflict : A git conflict with a start middle and end corresponding to line
|
||||
// numbers in the file where the conflict markers appear
|
||||
type mergeConflict struct {
|
||||
start int
|
||||
middle int
|
||||
end int
|
||||
}
|
||||
|
||||
type State struct {
|
||||
sync.Mutex
|
||||
conflictIndex int
|
||||
conflictTop bool
|
||||
conflicts []*mergeConflict
|
||||
EditHistory *stack.Stack
|
||||
}
|
||||
|
||||
func NewState() *State {
|
||||
return &State{
|
||||
Mutex: sync.Mutex{},
|
||||
conflictIndex: 0,
|
||||
conflictTop: true,
|
||||
conflicts: []*mergeConflict{},
|
||||
EditHistory: stack.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) SelectTopOption() {
|
||||
s.conflictTop = true
|
||||
}
|
||||
|
||||
func (s *State) SelectBottomOption() {
|
||||
s.conflictTop = false
|
||||
}
|
||||
|
||||
func (s *State) SelectNextConflict() {
|
||||
if s.conflictIndex < len(s.conflicts)-1 {
|
||||
s.conflictIndex++
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) SelectPrevConflict() {
|
||||
if s.conflictIndex > 0 {
|
||||
s.conflictIndex--
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) PushFileSnapshot(content string) {
|
||||
s.EditHistory.Push(content)
|
||||
}
|
||||
|
||||
func (s *State) PopFileSnapshot() (string, bool) {
|
||||
if s.EditHistory.Len() == 0 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return s.EditHistory.Pop().(string), true
|
||||
}
|
||||
|
||||
func (s *State) currentConflict() *mergeConflict {
|
||||
if len(s.conflicts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.conflicts[s.conflictIndex]
|
||||
}
|
||||
|
||||
func (s *State) SetConflictsFromCat(cat string) {
|
||||
s.setConflicts(findConflicts(cat))
|
||||
}
|
||||
|
||||
func (s *State) setConflicts(conflicts []*mergeConflict) {
|
||||
s.conflicts = conflicts
|
||||
|
||||
if s.conflictIndex > len(s.conflicts)-1 {
|
||||
s.conflictIndex = len(s.conflicts) - 1
|
||||
} else if s.conflictIndex < 0 {
|
||||
s.conflictIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) NoConflicts() bool {
|
||||
return len(s.conflicts) == 0
|
||||
}
|
||||
|
||||
func (s *State) Selection() Selection {
|
||||
if s.conflictTop {
|
||||
return TOP
|
||||
} else {
|
||||
return BOTTOM
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) IsFinalConflict() bool {
|
||||
return len(s.conflicts) == 1
|
||||
}
|
||||
|
||||
func (s *State) Reset() {
|
||||
s.EditHistory = stack.New()
|
||||
}
|
||||
|
||||
func (s *State) GetConflictMiddle() int {
|
||||
return s.currentConflict().middle
|
||||
}
|
||||
|
||||
func (s *State) ContentAfterConflictResolve(path string, selection Selection) (bool, string, error) {
|
||||
conflict := s.currentConflict()
|
||||
if conflict == nil {
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
content := ""
|
||||
err := utils.ForEachLineInFile(path, func(line string, i int) {
|
||||
if !isIndexToDelete(i, conflict, selection) {
|
||||
content += line
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
|
||||
return true, content, nil
|
||||
}
|
||||
|
||||
func isIndexToDelete(i int, conflict *mergeConflict, selection Selection) bool {
|
||||
isMarkerLine :=
|
||||
i == conflict.middle ||
|
||||
i == conflict.start ||
|
||||
i == conflict.end
|
||||
|
||||
isUnwantedContent :=
|
||||
(selection == BOTTOM && conflict.start < i && i < conflict.middle) ||
|
||||
(selection == TOP && conflict.middle < i && i < conflict.end)
|
||||
|
||||
return isMarkerLine || isUnwantedContent
|
||||
}
|
||||
103
pkg/gui/mergeconflicts/state_test.go
Normal file
103
pkg/gui/mergeconflicts/state_test.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package mergeconflicts
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFindConflicts(t *testing.T) {
|
||||
type scenario struct {
|
||||
name string
|
||||
content string
|
||||
expected []*mergeConflict
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
name: "empty",
|
||||
content: "",
|
||||
expected: []*mergeConflict{},
|
||||
},
|
||||
{
|
||||
name: "various conflicts",
|
||||
content: `++<<<<<<< HEAD
|
||||
foo
|
||||
++=======
|
||||
bar
|
||||
++>>>>>>> branch
|
||||
|
||||
<<<<<<< HEAD: foo/bar/baz.go
|
||||
foo
|
||||
bar
|
||||
=======
|
||||
baz
|
||||
>>>>>>> branch
|
||||
|
||||
++<<<<<<< MERGE_HEAD
|
||||
foo
|
||||
++=======
|
||||
bar
|
||||
++>>>>>>> branch
|
||||
|
||||
++<<<<<<< Updated upstream
|
||||
foo
|
||||
++=======
|
||||
bar
|
||||
++>>>>>>> branch
|
||||
|
||||
++<<<<<<< ours
|
||||
foo
|
||||
++=======
|
||||
bar
|
||||
++>>>>>>> branch
|
||||
|
||||
<<<<<<< Updated upstream: foo/bar/baz.go
|
||||
foo
|
||||
bar
|
||||
=======
|
||||
baz
|
||||
>>>>>>> branch
|
||||
`,
|
||||
expected: []*mergeConflict{
|
||||
{
|
||||
start: 0,
|
||||
middle: 2,
|
||||
end: 4,
|
||||
},
|
||||
{
|
||||
start: 6,
|
||||
middle: 9,
|
||||
end: 11,
|
||||
},
|
||||
{
|
||||
start: 13,
|
||||
middle: 15,
|
||||
end: 17,
|
||||
},
|
||||
{
|
||||
start: 19,
|
||||
middle: 21,
|
||||
end: 23,
|
||||
},
|
||||
{
|
||||
start: 25,
|
||||
middle: 27,
|
||||
end: 29,
|
||||
},
|
||||
{
|
||||
start: 31,
|
||||
middle: 34,
|
||||
end: 36,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
s := s
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
assert.EqualValues(t, s.expected, findConflicts(s.content))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ func (gui *Gui) getFromAndReverseArgsForDiff(to string) (string, bool) {
|
||||
return from, reverse
|
||||
}
|
||||
|
||||
func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int, state *lBlPanelState) error {
|
||||
func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int, state *LblPanelState) error {
|
||||
if !gui.GitCommand.PatchManager.Active() {
|
||||
return gui.handleEscapePatchBuildingPanel()
|
||||
}
|
||||
@@ -43,7 +43,7 @@ func (gui *Gui) refreshPatchBuildingPanel(selectedLineIdx int, state *lBlPanelSt
|
||||
return err
|
||||
}
|
||||
|
||||
empty, err := gui.refreshLineByLinePanel(diff, secondaryDiff, false, selectedLineIdx, state)
|
||||
empty, err := gui.refreshLineByLinePanel(diff, secondaryDiff, false, selectedLineIdx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -63,14 +63,14 @@ func (gui *Gui) handleRefreshPatchBuildingPanel(selectedLineIdx int) error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleToggleSelectionForPatch() error {
|
||||
err := gui.withLBLActiveCheck(func(state *lBlPanelState) error {
|
||||
err := gui.withLBLActiveCheck(func(state *LblPanelState) error {
|
||||
toggleFunc := gui.GitCommand.PatchManager.AddFileLineRange
|
||||
filename := gui.getSelectedCommitFileName()
|
||||
includedLineIndices, err := gui.GitCommand.PatchManager.GetFileIncLineIndices(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currentLineIsStaged := utils.IncludesInt(includedLineIndices, state.SelectedLineIdx)
|
||||
currentLineIsStaged := utils.IncludesInt(includedLineIndices, state.GetSelectedLineIdx())
|
||||
if currentLineIsStaged {
|
||||
toggleFunc = gui.GitCommand.PatchManager.RemoveFileLineRange
|
||||
}
|
||||
@@ -81,7 +81,9 @@ func (gui *Gui) handleToggleSelectionForPatch() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := toggleFunc(node.GetPath(), state.FirstLineIdx, state.LastLineIdx); err != nil {
|
||||
firstLineIdx, lastLineIdx := state.SelectedRange()
|
||||
|
||||
if err := toggleFunc(node.GetPath(), firstLineIdx, lastLineIdx); err != nil {
|
||||
// might actually want to return an error here
|
||||
gui.Log.Error(err)
|
||||
}
|
||||
|
||||
@@ -33,11 +33,11 @@ func (gui *Gui) handleCreatePatchOptionsMenu() error {
|
||||
onPress: gui.handleDeletePatchFromCommit,
|
||||
},
|
||||
{
|
||||
displayString: "pull patch out into index",
|
||||
onPress: gui.handlePullPatchIntoWorkingTree,
|
||||
displayString: "move patch out into index",
|
||||
onPress: gui.handleMovePatchIntoWorkingTree,
|
||||
},
|
||||
{
|
||||
displayString: "pull patch into new commit",
|
||||
displayString: "move patch into new commit",
|
||||
onPress: gui.handlePullPatchIntoNewCommit,
|
||||
},
|
||||
}...)
|
||||
@@ -98,7 +98,7 @@ func (gui *Gui) handleDeletePatchFromCommit() error {
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error {
|
||||
commitIndex := gui.getPatchCommitIndex()
|
||||
err := gui.GitCommand.DeletePatchesFromCommit(gui.State.Commits, commitIndex, gui.GitCommand.PatchManager)
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.RemovePatchFromCommit).DeletePatchesFromCommit(gui.State.Commits, commitIndex, gui.GitCommand.PatchManager)
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
}
|
||||
@@ -114,12 +114,12 @@ func (gui *Gui) handleMovePatchToSelectedCommit() error {
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error {
|
||||
commitIndex := gui.getPatchCommitIndex()
|
||||
err := gui.GitCommand.MovePatchToSelectedCommit(gui.State.Commits, commitIndex, gui.State.Panels.Commits.SelectedLineIdx, gui.GitCommand.PatchManager)
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.MovePatchToSelectedCommit).MovePatchToSelectedCommit(gui.State.Commits, commitIndex, gui.State.Panels.Commits.SelectedLineIdx, gui.GitCommand.PatchManager)
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handlePullPatchIntoWorkingTree() error {
|
||||
func (gui *Gui) handleMovePatchIntoWorkingTree() error {
|
||||
if ok, err := gui.validateNormalWorkingTreeState(); !ok {
|
||||
return err
|
||||
}
|
||||
@@ -131,7 +131,7 @@ func (gui *Gui) handlePullPatchIntoWorkingTree() error {
|
||||
pull := func(stash bool) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error {
|
||||
commitIndex := gui.getPatchCommitIndex()
|
||||
err := gui.GitCommand.PullPatchIntoIndex(gui.State.Commits, commitIndex, gui.GitCommand.PatchManager, stash)
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.MovePatchIntoIndex).MovePatchIntoIndex(gui.State.Commits, commitIndex, gui.GitCommand.PatchManager, stash)
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
}
|
||||
@@ -160,7 +160,7 @@ func (gui *Gui) handlePullPatchIntoNewCommit() error {
|
||||
|
||||
return gui.WithWaitingStatus(gui.Tr.RebasingStatus, func() error {
|
||||
commitIndex := gui.getPatchCommitIndex()
|
||||
err := gui.GitCommand.PullPatchIntoNewCommit(gui.State.Commits, commitIndex, gui.GitCommand.PatchManager)
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.MovePatchIntoNewCommit).PullPatchIntoNewCommit(gui.State.Commits, commitIndex, gui.GitCommand.PatchManager)
|
||||
return gui.handleGenericMergeCommandResult(err)
|
||||
})
|
||||
}
|
||||
@@ -170,7 +170,11 @@ func (gui *Gui) handleApplyPatch(reverse bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.GitCommand.PatchManager.ApplyPatches(reverse); err != nil {
|
||||
span := gui.Tr.Spans.ApplyPatch
|
||||
if reverse {
|
||||
span = "Apply patch in reverse"
|
||||
}
|
||||
if err := gui.GitCommand.WithSpan(span).PatchManager.ApplyPatches(reverse); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC})
|
||||
|
||||
@@ -43,18 +43,20 @@ func (gui *Gui) genericMergeCommand(command string) error {
|
||||
return gui.createErrorPanel(gui.Tr.NotMergingOrRebasing)
|
||||
}
|
||||
|
||||
gitCommand := gui.GitCommand.WithSpan(fmt.Sprintf("Merge/Rebase: %s", command))
|
||||
|
||||
commandType := strings.Replace(status, "ing", "e", 1)
|
||||
// we should end up with a command like 'git merge --continue'
|
||||
|
||||
// it's impossible for a rebase to require a commit so we'll use a subprocess only if it's a merge
|
||||
if status == commands.REBASE_MODE_MERGING && command != "abort" && gui.Config.GetUserConfig().Git.Merging.ManualCommit {
|
||||
sub := gui.OSCommand.PrepareSubProcess("git", commandType, fmt.Sprintf("--%s", command))
|
||||
sub := gitCommand.OSCommand.PrepareSubProcess("git", commandType, fmt.Sprintf("--%s", command))
|
||||
if sub != nil {
|
||||
return gui.runSubprocessWithSuspense(sub)
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(sub)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
result := gui.GitCommand.GenericMergeOrRebaseAction(commandType, command)
|
||||
result := gitCommand.GenericMergeOrRebaseAction(commandType, command)
|
||||
if err := gui.handleGenericMergeCommandResult(result); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ func (gui *Gui) handleCheckoutReflogCommit() error {
|
||||
title: gui.Tr.LcCheckoutCommit,
|
||||
prompt: gui.Tr.SureCheckoutThisCommit,
|
||||
handleConfirm: func() error {
|
||||
return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{})
|
||||
return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{span: gui.Tr.Spans.CheckoutReflogCommit})
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -59,7 +59,7 @@ func (gui *Gui) handleDeleteRemoteBranch() error {
|
||||
prompt: message,
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.DeletingStatus, func() error {
|
||||
err := gui.GitCommand.DeleteRemoteBranch(remoteBranch.RemoteName, remoteBranch.Name, gui.promptUserForCredential)
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.DeleteRemoteBranch).DeleteRemoteBranch(remoteBranch.RemoteName, remoteBranch.Name, gui.promptUserForCredential)
|
||||
gui.handleCredentialsPopup(err)
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, REMOTES}})
|
||||
@@ -89,7 +89,7 @@ func (gui *Gui) handleSetBranchUpstream() error {
|
||||
title: gui.Tr.SetUpstreamTitle,
|
||||
prompt: message,
|
||||
handleConfirm: func() error {
|
||||
if err := gui.GitCommand.SetBranchUpstream(selectedBranch.RemoteName, selectedBranch.Name, checkedOutBranch.Name); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.SetBranchUpstream).SetBranchUpstream(selectedBranch.RemoteName, selectedBranch.Name, checkedOutBranch.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ func (gui *Gui) handleAddRemote() error {
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.LcNewRemoteUrl,
|
||||
handleConfirm: func(remoteUrl string) error {
|
||||
if err := gui.GitCommand.AddRemote(remoteName, remoteUrl); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.AddRemote).AddRemote(remoteName, remoteUrl); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{REMOTES}})
|
||||
@@ -106,7 +106,7 @@ func (gui *Gui) handleRemoveRemote() error {
|
||||
title: gui.Tr.LcRemoveRemote,
|
||||
prompt: gui.Tr.LcRemoveRemotePrompt + " '" + remote.Name + "'?",
|
||||
handleConfirm: func() error {
|
||||
if err := gui.GitCommand.RemoveRemote(remote.Name); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.RemoveRemote).RemoveRemote(remote.Name); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
@@ -128,12 +128,14 @@ func (gui *Gui) handleEditRemote() error {
|
||||
},
|
||||
)
|
||||
|
||||
gitCommand := gui.GitCommand.WithSpan(gui.Tr.Spans.UpdateRemote)
|
||||
|
||||
return gui.prompt(promptOpts{
|
||||
title: editNameMessage,
|
||||
initialContent: remote.Name,
|
||||
handleConfirm: func(updatedRemoteName string) error {
|
||||
if updatedRemoteName != remote.Name {
|
||||
if err := gui.GitCommand.RenameRemote(remote.Name, updatedRemoteName); err != nil {
|
||||
if err := gitCommand.RenameRemote(remote.Name, updatedRemoteName); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
}
|
||||
@@ -155,7 +157,7 @@ func (gui *Gui) handleEditRemote() error {
|
||||
title: editUrlMessage,
|
||||
initialContent: url,
|
||||
handleConfirm: func(updatedRemoteUrl string) error {
|
||||
if err := gui.GitCommand.UpdateRemoteUrl(updatedRemoteName, updatedRemoteUrl); err != nil {
|
||||
if err := gitCommand.UpdateRemoteUrl(updatedRemoteName, updatedRemoteUrl); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{BRANCHES, REMOTES}})
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
)
|
||||
|
||||
func (gui *Gui) resetToRef(ref string, strength string, options oscommands.RunCommandOptions) error {
|
||||
if err := gui.GitCommand.ResetToCommit(ref, strength, options); err != nil {
|
||||
func (gui *Gui) resetToRef(ref string, strength string, span string, options oscommands.RunCommandOptions) error {
|
||||
if err := gui.GitCommand.WithSpan(span).ResetToCommit(ref, strength, options); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ func (gui *Gui) createResetMenu(ref string) error {
|
||||
),
|
||||
},
|
||||
onPress: func() error {
|
||||
return gui.resetToRef(ref, strength, oscommands.RunCommandOptions{})
|
||||
return gui.resetToRef(ref, strength, "Reset", oscommands.RunCommandOptions{})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ func (gui *Gui) handleOpenSearch(viewName string) error {
|
||||
|
||||
func (gui *Gui) handleSearch() error {
|
||||
gui.State.Searching.searchString = gui.Views.Search.Buffer()
|
||||
gui.Log.Warn(gui.State.Searching.searchString)
|
||||
if err := gui.returnFromContext(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
)
|
||||
|
||||
func (gui *Gui) refreshStagingPanel(forceSecondaryFocused bool, selectedLineIdx int, state *lBlPanelState) error {
|
||||
func (gui *Gui) refreshStagingPanel(forceSecondaryFocused bool, selectedLineIdx int) error {
|
||||
gui.splitMainPanel(true)
|
||||
|
||||
file := gui.getSelectedFile()
|
||||
@@ -17,8 +17,8 @@ func (gui *Gui) refreshStagingPanel(forceSecondaryFocused bool, selectedLineIdx
|
||||
secondaryFocused := false
|
||||
if forceSecondaryFocused {
|
||||
secondaryFocused = true
|
||||
} else if state != nil {
|
||||
secondaryFocused = state.SecondaryFocused
|
||||
} else if gui.State.Panels.LineByLine != nil {
|
||||
secondaryFocused = gui.State.Panels.LineByLine.SecondaryFocused
|
||||
}
|
||||
|
||||
if (secondaryFocused && !file.HasStagedChanges) || (!secondaryFocused && !file.HasUnstagedChanges) {
|
||||
@@ -47,7 +47,7 @@ func (gui *Gui) refreshStagingPanel(forceSecondaryFocused bool, selectedLineIdx
|
||||
diff, secondaryDiff = secondaryDiff, diff
|
||||
}
|
||||
|
||||
empty, err := gui.refreshLineByLinePanel(diff, secondaryDiff, secondaryFocused, selectedLineIdx, state)
|
||||
empty, err := gui.refreshLineByLinePanel(diff, secondaryDiff, secondaryFocused, selectedLineIdx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -60,10 +60,10 @@ func (gui *Gui) refreshStagingPanel(forceSecondaryFocused bool, selectedLineIdx
|
||||
}
|
||||
|
||||
func (gui *Gui) handleTogglePanelClick() error {
|
||||
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
|
||||
return gui.withLBLActiveCheck(func(state *LblPanelState) error {
|
||||
state.SecondaryFocused = !state.SecondaryFocused
|
||||
|
||||
return gui.refreshStagingPanel(false, gui.Views.Secondary.SelectedLineIdx(), state)
|
||||
return gui.refreshStagingPanel(false, gui.Views.Secondary.SelectedLineIdx())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -71,13 +71,13 @@ func (gui *Gui) handleRefreshStagingPanel(forceSecondaryFocused bool, selectedLi
|
||||
gui.Mutexes.LineByLinePanelMutex.Lock()
|
||||
defer gui.Mutexes.LineByLinePanelMutex.Unlock()
|
||||
|
||||
return gui.refreshStagingPanel(forceSecondaryFocused, selectedLineIdx, gui.State.Panels.LineByLine)
|
||||
return gui.refreshStagingPanel(forceSecondaryFocused, selectedLineIdx)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleTogglePanel() error {
|
||||
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
|
||||
return gui.withLBLActiveCheck(func(state *LblPanelState) error {
|
||||
state.SecondaryFocused = !state.SecondaryFocused
|
||||
return gui.refreshStagingPanel(false, -1, state)
|
||||
return gui.refreshStagingPanel(false, -1)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -88,13 +88,13 @@ func (gui *Gui) handleStagingEscape() error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleToggleStagedSelection() error {
|
||||
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
|
||||
return gui.withLBLActiveCheck(func(state *LblPanelState) error {
|
||||
return gui.applySelection(state.SecondaryFocused, state)
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) handleResetSelection() error {
|
||||
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
|
||||
return gui.withLBLActiveCheck(func(state *LblPanelState) error {
|
||||
if state.SecondaryFocused {
|
||||
// for backwards compatibility
|
||||
return gui.applySelection(true, state)
|
||||
@@ -106,7 +106,7 @@ func (gui *Gui) handleResetSelection() error {
|
||||
prompt: gui.Tr.UnstageLinesPrompt,
|
||||
handlersManageFocus: true,
|
||||
handleConfirm: func() error {
|
||||
return gui.withLBLActiveCheck(func(state *lBlPanelState) error {
|
||||
return gui.withLBLActiveCheck(func(state *LblPanelState) error {
|
||||
if err := gui.pushContext(gui.State.Contexts.Staging); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -124,13 +124,14 @@ func (gui *Gui) handleResetSelection() error {
|
||||
})
|
||||
}
|
||||
|
||||
func (gui *Gui) applySelection(reverse bool, state *lBlPanelState) error {
|
||||
func (gui *Gui) applySelection(reverse bool, state *LblPanelState) error {
|
||||
file := gui.getSelectedFile()
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
patch := patch.ModifiedPatchForRange(gui.Log, file.Name, state.Diff, state.FirstLineIdx, state.LastLineIdx, reverse, false)
|
||||
firstLineIdx, lastLineIdx := state.SelectedRange()
|
||||
patch := patch.ModifiedPatchForRange(gui.Log, file.Name, state.GetDiff(), firstLineIdx, lastLineIdx, reverse, false)
|
||||
|
||||
if patch == "" {
|
||||
return nil
|
||||
@@ -142,19 +143,19 @@ func (gui *Gui) applySelection(reverse bool, state *lBlPanelState) error {
|
||||
if !reverse || state.SecondaryFocused {
|
||||
applyFlags = append(applyFlags, "cached")
|
||||
}
|
||||
err := gui.GitCommand.ApplyPatch(patch, applyFlags...)
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.ApplyPatch).ApplyPatch(patch, applyFlags...)
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
if state.SelectMode == RANGE {
|
||||
state.SelectMode = LINE
|
||||
if state.SelectingRange() {
|
||||
state.SetLineSelectMode()
|
||||
}
|
||||
|
||||
if err := gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gui.refreshStagingPanel(false, -1, state); err != nil {
|
||||
if err := gui.refreshStagingPanel(false, -1); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -106,7 +106,7 @@ func (gui *Gui) stashDo(method string) error {
|
||||
|
||||
return gui.createErrorPanel(errorMessage)
|
||||
}
|
||||
if err := gui.GitCommand.StashDo(stashEntry.Index, method); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.Stash).StashDo(stashEntry.Index, method); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{STASH, FILES}})
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/constants"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
@@ -42,10 +42,7 @@ func (gui *Gui) refreshStatus() {
|
||||
repoName := utils.GetCurrentRepoName()
|
||||
status += fmt.Sprintf("%s → %s ", repoName, name)
|
||||
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
gui.setViewContent(gui.Views.Status, status)
|
||||
return nil
|
||||
})
|
||||
gui.setViewContent(gui.Views.Status, status)
|
||||
}
|
||||
|
||||
func runeCount(str string) int {
|
||||
@@ -110,12 +107,12 @@ func (gui *Gui) handleStatusSelect() error {
|
||||
[]string{
|
||||
lazygitTitle(),
|
||||
"Copyright (c) 2018 Jesse Duffield",
|
||||
"Keybindings: https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings",
|
||||
"Config Options: https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md",
|
||||
"Tutorial: https://youtu.be/VDXvbHZYeKY",
|
||||
"Raise an Issue: https://github.com/jesseduffield/lazygit/issues",
|
||||
magenta.Sprint("Become a sponsor (github is matching all donations for 12 months): https://github.com/sponsors/jesseduffield"), // caffeine ain't free
|
||||
gui.Tr.ReleaseNotes,
|
||||
fmt.Sprintf("Keybindings: %s", constants.Links.Docs.Keybindings),
|
||||
fmt.Sprintf("Config Options: %s", constants.Links.Docs.Config),
|
||||
fmt.Sprintf("Tutorial: %s", constants.Links.Docs.Tutorial),
|
||||
fmt.Sprintf("Raise an Issue: %s", constants.Links.Issues),
|
||||
fmt.Sprintf("Release Notes: %s", constants.Links.Releases),
|
||||
magenta.Sprintf("Become a sponsor (github is matching all donations for 12 months): %s", constants.Links.Donate), // caffeine ain't free
|
||||
}, "\n\n")
|
||||
|
||||
return gui.refreshMainViews(refreshMainOpts{
|
||||
|
||||
@@ -48,7 +48,7 @@ func (gui *Gui) handleCheckoutSubCommit() error {
|
||||
title: gui.Tr.LcCheckoutCommit,
|
||||
prompt: gui.Tr.SureCheckoutThisCommit,
|
||||
handleConfirm: func() error {
|
||||
return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{})
|
||||
return gui.handleCheckoutRef(commit.Sha, handleCheckoutRefOptions{span: gui.Tr.Spans.CheckoutCommit})
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -81,7 +81,7 @@ func (gui *Gui) removeSubmodule(submodule *models.SubmoduleConfig) error {
|
||||
title: gui.Tr.RemoveSubmodule,
|
||||
prompt: fmt.Sprintf(gui.Tr.RemoveSubmodulePrompt, submodule.Name),
|
||||
handleConfirm: func() error {
|
||||
if err := gui.GitCommand.SubmoduleDelete(submodule); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.RemoveSubmodule).SubmoduleDelete(submodule); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
@@ -107,17 +107,19 @@ func (gui *Gui) fileForSubmodule(submodule *models.SubmoduleConfig) *models.File
|
||||
}
|
||||
|
||||
func (gui *Gui) resetSubmodule(submodule *models.SubmoduleConfig) error {
|
||||
gitCommand := gui.GitCommand.WithSpan(gui.Tr.Spans.ResetSubmodule)
|
||||
|
||||
file := gui.fileForSubmodule(submodule)
|
||||
if file != nil {
|
||||
if err := gui.GitCommand.UnStageFile(file.Names(), file.Tracked); err != nil {
|
||||
if err := gitCommand.UnStageFile(file.Names(), file.Tracked); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := gui.GitCommand.SubmoduleStash(submodule); err != nil {
|
||||
if err := gitCommand.SubmoduleStash(submodule); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
if err := gui.GitCommand.SubmoduleReset(submodule); err != nil {
|
||||
if err := gitCommand.SubmoduleReset(submodule); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
@@ -140,7 +142,7 @@ func (gui *Gui) handleAddSubmodule() error {
|
||||
initialContent: submoduleName,
|
||||
handleConfirm: func(submodulePath string) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.LcAddingSubmoduleStatus, func() error {
|
||||
err := gui.GitCommand.SubmoduleAdd(submoduleName, submodulePath, submoduleUrl)
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.AddSubmodule).SubmoduleAdd(submoduleName, submodulePath, submoduleUrl)
|
||||
gui.handleCredentialsPopup(err)
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}})
|
||||
@@ -160,7 +162,7 @@ func (gui *Gui) handleEditSubmoduleUrl(submodule *models.SubmoduleConfig) error
|
||||
initialContent: submodule.Url,
|
||||
handleConfirm: func(newUrl string) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.LcUpdatingSubmoduleUrlStatus, func() error {
|
||||
err := gui.GitCommand.SubmoduleUpdateUrl(submodule.Name, submodule.Path, newUrl)
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.UpdateSubmoduleUrl).SubmoduleUpdateUrl(submodule.Name, submodule.Path, newUrl)
|
||||
gui.handleCredentialsPopup(err)
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}})
|
||||
@@ -171,7 +173,7 @@ func (gui *Gui) handleEditSubmoduleUrl(submodule *models.SubmoduleConfig) error
|
||||
|
||||
func (gui *Gui) handleSubmoduleInit(submodule *models.SubmoduleConfig) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.LcInitializingSubmoduleStatus, func() error {
|
||||
err := gui.GitCommand.SubmoduleInit(submodule.Path)
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.InitialiseSubmodule).SubmoduleInit(submodule.Path)
|
||||
gui.handleCredentialsPopup(err)
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}})
|
||||
@@ -214,7 +216,7 @@ func (gui *Gui) handleBulkSubmoduleActionsMenu() error {
|
||||
displayStrings: []string{gui.Tr.LcBulkInitSubmodules, utils.ColoredString(gui.GitCommand.SubmoduleBulkInitCmdStr(), color.FgGreen)},
|
||||
onPress: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.LcRunningCommand, func() error {
|
||||
if err := gui.OSCommand.RunCommand(gui.GitCommand.SubmoduleBulkInitCmdStr()); err != nil {
|
||||
if err := gui.OSCommand.WithSpan(gui.Tr.Spans.BulkInitialiseSubmodules).RunCommand(gui.GitCommand.SubmoduleBulkInitCmdStr()); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
@@ -226,7 +228,7 @@ func (gui *Gui) handleBulkSubmoduleActionsMenu() error {
|
||||
displayStrings: []string{gui.Tr.LcBulkUpdateSubmodules, utils.ColoredString(gui.GitCommand.SubmoduleBulkUpdateCmdStr(), color.FgYellow)},
|
||||
onPress: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.LcRunningCommand, func() error {
|
||||
if err := gui.OSCommand.RunCommand(gui.GitCommand.SubmoduleBulkUpdateCmdStr()); err != nil {
|
||||
if err := gui.OSCommand.WithSpan(gui.Tr.Spans.BulkUpdateSubmodules).RunCommand(gui.GitCommand.SubmoduleBulkUpdateCmdStr()); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
@@ -238,7 +240,7 @@ func (gui *Gui) handleBulkSubmoduleActionsMenu() error {
|
||||
displayStrings: []string{gui.Tr.LcSubmoduleStashAndReset, utils.ColoredString(fmt.Sprintf("git stash in each submodule && %s", gui.GitCommand.SubmoduleForceBulkUpdateCmdStr()), color.FgRed)},
|
||||
onPress: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.LcRunningCommand, func() error {
|
||||
if err := gui.GitCommand.ResetSubmodules(gui.State.Submodules); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.BulkStashAndResetSubmodules).ResetSubmodules(gui.State.Submodules); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
@@ -250,7 +252,7 @@ func (gui *Gui) handleBulkSubmoduleActionsMenu() error {
|
||||
displayStrings: []string{gui.Tr.LcBulkDeinitSubmodules, utils.ColoredString(gui.GitCommand.SubmoduleBulkDeinitCmdStr(), color.FgRed)},
|
||||
onPress: func() error {
|
||||
return gui.WithWaitingStatus(gui.Tr.LcRunningCommand, func() error {
|
||||
if err := gui.OSCommand.RunCommand(gui.GitCommand.SubmoduleBulkDeinitCmdStr()); err != nil {
|
||||
if err := gui.OSCommand.WithSpan(gui.Tr.Spans.BulkDeinitialiseSubmodules).RunCommand(gui.GitCommand.SubmoduleBulkDeinitCmdStr()); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
@@ -265,7 +267,7 @@ func (gui *Gui) handleBulkSubmoduleActionsMenu() error {
|
||||
|
||||
func (gui *Gui) handleUpdateSubmodule(submodule *models.SubmoduleConfig) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.LcUpdatingSubmoduleStatus, func() error {
|
||||
err := gui.GitCommand.SubmoduleUpdate(submodule.Path)
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.UpdateSubmodule).SubmoduleUpdate(submodule.Path)
|
||||
gui.handleCredentialsPopup(err)
|
||||
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}})
|
||||
|
||||
@@ -19,7 +19,7 @@ func (gui *Gui) handleCreateTag() error {
|
||||
title: gui.Tr.CreateTagTitle,
|
||||
handleConfirm: func(tagName string) error {
|
||||
// leaving commit SHA blank so that we're just creating the tag for the current commit
|
||||
if err := gui.GitCommand.CreateLightweightTag(tagName, ""); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.CreateLightweightTag).CreateLightweightTag(tagName, ""); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{COMMITS, TAGS}, then: func() {
|
||||
@@ -86,7 +86,7 @@ func (gui *Gui) withSelectedTag(f func(tag *models.Tag) error) func() error {
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCheckoutTag(tag *models.Tag) error {
|
||||
if err := gui.handleCheckoutRef(tag.Name, handleCheckoutRefOptions{}); err != nil {
|
||||
if err := gui.handleCheckoutRef(tag.Name, handleCheckoutRefOptions{span: gui.Tr.Spans.CheckoutTag}); err != nil {
|
||||
return err
|
||||
}
|
||||
return gui.pushContext(gui.State.Contexts.Branches)
|
||||
@@ -104,7 +104,7 @@ func (gui *Gui) handleDeleteTag(tag *models.Tag) error {
|
||||
title: gui.Tr.DeleteTagTitle,
|
||||
prompt: prompt,
|
||||
handleConfirm: func() error {
|
||||
if err := gui.GitCommand.DeleteTag(tag.Name); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.DeleteTag).DeleteTag(tag.Name); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{COMMITS, TAGS}})
|
||||
@@ -125,7 +125,7 @@ func (gui *Gui) handlePushTag(tag *models.Tag) error {
|
||||
initialContent: "origin",
|
||||
handleConfirm: func(response string) error {
|
||||
return gui.WithWaitingStatus(gui.Tr.PushingTagStatus, func() error {
|
||||
err := gui.GitCommand.PushTag(response, tag.Name, gui.promptUserForCredential)
|
||||
err := gui.GitCommand.WithSpan(gui.Tr.Spans.PushTag).PushTag(response, tag.Name, gui.promptUserForCredential)
|
||||
gui.handleCredentialsPopup(err)
|
||||
|
||||
return nil
|
||||
|
||||
@@ -89,7 +89,7 @@ func (gui *Gui) getManager(view *gocui.View) *tasks.ViewBufferManager {
|
||||
// overwriting the existing content from the top down. Once we've reached
|
||||
// the end of the content do display, we call view.FlushStaleCells() to
|
||||
// clear out the remaining content from the previous render.
|
||||
view.Rewind()
|
||||
view.Reset()
|
||||
},
|
||||
func() {
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
|
||||
@@ -93,6 +93,8 @@ func (gui *Gui) reflogUndo() error {
|
||||
return gui.createErrorPanel(gui.Tr.LcCantUndoWhileRebasing)
|
||||
}
|
||||
|
||||
span := gui.Tr.Spans.Undo
|
||||
|
||||
return gui.parseReflogForActions(func(counter int, action reflogAction) (bool, error) {
|
||||
if counter != 0 {
|
||||
return false, nil
|
||||
@@ -103,11 +105,13 @@ func (gui *Gui) reflogUndo() error {
|
||||
return true, gui.handleHardResetWithAutoStash(action.from, handleHardResetWithAutoStashOptions{
|
||||
EnvVars: undoEnvVars,
|
||||
WaitingStatus: undoingStatus,
|
||||
span: span,
|
||||
})
|
||||
case CHECKOUT:
|
||||
return true, gui.handleCheckoutRef(action.from, handleCheckoutRefOptions{
|
||||
EnvVars: undoEnvVars,
|
||||
WaitingStatus: undoingStatus,
|
||||
span: span,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -124,6 +128,8 @@ func (gui *Gui) reflogRedo() error {
|
||||
return gui.createErrorPanel(gui.Tr.LcCantRedoWhileRebasing)
|
||||
}
|
||||
|
||||
span := gui.Tr.Spans.Redo
|
||||
|
||||
return gui.parseReflogForActions(func(counter int, action reflogAction) (bool, error) {
|
||||
// if we're redoing and the counter is zero, we just return
|
||||
if counter == 0 {
|
||||
@@ -137,11 +143,13 @@ func (gui *Gui) reflogRedo() error {
|
||||
return true, gui.handleHardResetWithAutoStash(action.to, handleHardResetWithAutoStashOptions{
|
||||
EnvVars: redoEnvVars,
|
||||
WaitingStatus: redoingStatus,
|
||||
span: span,
|
||||
})
|
||||
case CHECKOUT:
|
||||
return true, gui.handleCheckoutRef(action.to, handleCheckoutRefOptions{
|
||||
EnvVars: redoEnvVars,
|
||||
WaitingStatus: redoingStatus,
|
||||
span: span,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -153,19 +161,22 @@ func (gui *Gui) reflogRedo() error {
|
||||
type handleHardResetWithAutoStashOptions struct {
|
||||
WaitingStatus string
|
||||
EnvVars []string
|
||||
span string
|
||||
}
|
||||
|
||||
// only to be used in the undo flow for now
|
||||
func (gui *Gui) handleHardResetWithAutoStash(commitSha string, options handleHardResetWithAutoStashOptions) error {
|
||||
gitCommand := gui.GitCommand.WithSpan(options.span)
|
||||
|
||||
reset := func() error {
|
||||
if err := gui.resetToRef(commitSha, "hard", oscommands.RunCommandOptions{EnvVars: options.EnvVars}); err != nil {
|
||||
if err := gui.resetToRef(commitSha, "hard", options.span, oscommands.RunCommandOptions{EnvVars: options.EnvVars}); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// if we have any modified tracked files we need to ask the user if they want us to stash for them
|
||||
dirtyWorkingTree := len(gui.trackedFiles()) > 0
|
||||
dirtyWorkingTree := len(gui.trackedFiles()) > 0 || len(gui.stagedFiles()) > 0
|
||||
if dirtyWorkingTree {
|
||||
// offer to autostash changes
|
||||
return gui.ask(askOpts{
|
||||
@@ -173,17 +184,18 @@ func (gui *Gui) handleHardResetWithAutoStash(commitSha string, options handleHar
|
||||
prompt: gui.Tr.AutoStashPrompt,
|
||||
handleConfirm: func() error {
|
||||
return gui.WithWaitingStatus(options.WaitingStatus, func() error {
|
||||
if err := gui.GitCommand.StashSave(gui.Tr.StashPrefix + commitSha); err != nil {
|
||||
if err := gitCommand.StashSave(gui.Tr.StashPrefix + commitSha); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
if err := reset(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gui.GitCommand.StashDo(0, "pop"); err != nil {
|
||||
if err := gui.refreshSidePanels(refreshOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
err := gitCommand.StashDo(0, "pop")
|
||||
if err := gui.refreshSidePanels(refreshOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
return nil
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user