mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-29 18:46:55 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
+241
-235
@@ -2,189 +2,192 @@
|
||||
|
||||
Default path for the config file:
|
||||
|
||||
* Linux: `~/.config/lazygit/config.yml`
|
||||
* MacOS: `~/Library/Application Support/lazygit/config.yml`
|
||||
* Windows: `%APPDATA%\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`
|
||||
- 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
|
||||
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 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
|
||||
@@ -192,22 +195,22 @@ For old installations (slightly embarrassing: I didn't realise at the time that
|
||||
### 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}}'
|
||||
```
|
||||
|
||||
### Recommended Config Values
|
||||
@@ -215,8 +218,8 @@ For old installations (slightly embarrassing: I didn't realise at the time that
|
||||
for users of VSCode
|
||||
|
||||
```yaml
|
||||
os:
|
||||
openCommand: 'code -rg {{filename}}'
|
||||
os:
|
||||
openCommand: 'code -rg {{filename}}'
|
||||
```
|
||||
|
||||
## Color Attributes
|
||||
@@ -242,16 +245,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
|
||||
@@ -259,15 +262,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:
|
||||
@@ -296,33 +300,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
|
||||
@@ -333,7 +337,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:
|
||||
@@ -343,19 +347,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
|
||||
|
||||
@@ -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.20210410011117-a2bb4baca390
|
||||
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,7 +39,7 @@ 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/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
|
||||
)
|
||||
|
||||
@@ -110,6 +110,16 @@ github.com/jesseduffield/gocui v0.3.1-0.20210409121040-210802112d8a h1:ocrSuZxQI
|
||||
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=
|
||||
@@ -230,6 +240,8 @@ 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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
@@ -53,11 +52,6 @@ func (c *GitCommand) AmendHead() error {
|
||||
return c.OSCommand.RunCommand(c.AmendHeadCmdStr())
|
||||
}
|
||||
|
||||
// PrepareCommitAmendHeadSubProcess prepares a subprocess for `git commit --amend --allow-empty`
|
||||
func (c *GitCommand) PrepareCommitAmendHeadSubProcess() *exec.Cmd {
|
||||
return c.OSCommand.ShellCommandFromString(c.AmendHeadCmdStr())
|
||||
}
|
||||
|
||||
func (c *GitCommand) AmendHeadCmdStr() string {
|
||||
return "git commit --amend --no-edit --allow-empty"
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
}
|
||||
}
|
||||
|
||||
+1
-385
@@ -2,20 +2,14 @@ package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/jesseduffield/lazygit/pkg/test"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// CatFile obtains the content of a file
|
||||
@@ -145,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)
|
||||
}
|
||||
@@ -346,381 +340,3 @@ func (c *GitCommand) EditFileCmdStr(filename string) (string, error) {
|
||||
|
||||
return fmt.Sprintf("%s %s", editor, c.OSCommand.Quote(filename)), nil
|
||||
}
|
||||
|
||||
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
|
||||
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 $GIT_EDITOR, $VISUAL, $EDITOR, or git config")
|
||||
},
|
||||
},
|
||||
{
|
||||
"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.OSCommand.Command = s.command
|
||||
gitCmd.OSCommand.Getenv = s.getenv
|
||||
gitCmd.getGitConfigValue = s.getGitConfigValue
|
||||
s.test(gitCmd.EditFileCmdStr(s.filename))
|
||||
}
|
||||
}
|
||||
|
||||
+380
-1
@@ -2,6 +2,7 @@ package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"testing"
|
||||
@@ -323,7 +324,7 @@ func TestGitCommandDiscardAllFileChanges(t *testing.T) {
|
||||
var cmdsCalled *[][]string
|
||||
gitCmd := NewDummyGitCommand()
|
||||
gitCmd.OSCommand.Command, cmdsCalled = s.command()
|
||||
gitCmd.removeFile = s.removeFile
|
||||
gitCmd.OSCommand.SetRemoveFile(s.removeFile)
|
||||
s.test(cmdsCalled, gitCmd.DiscardAllFileChanges(s.file))
|
||||
})
|
||||
}
|
||||
@@ -465,3 +466,381 @@ func TestGitCommandCheckoutFile(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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 $GIT_EDITOR, $VISUAL, $EDITOR, or git config")
|
||||
},
|
||||
},
|
||||
{
|
||||
"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.OSCommand.Command = s.command
|
||||
gitCmd.OSCommand.Getenv = s.getenv
|
||||
gitCmd.getGitConfigValue = s.getGitConfigValue
|
||||
s.test(gitCmd.EditFileCmdStr(s.filename))
|
||||
}
|
||||
}
|
||||
|
||||
+21
-2
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
|
||||
+111
-18
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
@@ -156,6 +162,7 @@ type KeybindingUniversalConfig struct {
|
||||
CopyToClipboard string `yaml:"copyToClipboard"`
|
||||
SubmitEditorText string `yaml:"submitEditorText"`
|
||||
AppendNewline string `yaml:"appendNewline"`
|
||||
ExtrasMenu string `yaml:"extrasMenu"`
|
||||
}
|
||||
|
||||
type KeybindingStatusConfig struct {
|
||||
@@ -295,6 +302,10 @@ func GetDefaultConfig() *UserConfig {
|
||||
},
|
||||
CommitLength: CommitLengthConfig{Show: true},
|
||||
SkipNoStagedFilesWarning: false,
|
||||
ShowCommandLog: true,
|
||||
ShowFileTree: false,
|
||||
ShowRandomTip: true,
|
||||
CommandLogSize: 8,
|
||||
},
|
||||
Git: GitConfig{
|
||||
Paging: PagingConfig{
|
||||
@@ -346,6 +357,8 @@ func GetDefaultConfig() *UserConfig {
|
||||
NextBlock: "<right>",
|
||||
PrevBlockAlt: "h",
|
||||
NextBlockAlt: "l",
|
||||
PrevBlockAlt2: "<backtab>",
|
||||
NextBlockAlt2: "<tab>",
|
||||
NextMatch: "n",
|
||||
PrevMatch: "N",
|
||||
StartSearch: "/",
|
||||
@@ -383,6 +396,7 @@ func GetDefaultConfig() *UserConfig {
|
||||
CopyToClipboard: "<c-o>",
|
||||
SubmitEditorText: "<enter>",
|
||||
AppendNewline: "<a-enter>",
|
||||
ExtrasMenu: "@",
|
||||
},
|
||||
Status: KeybindingStatusConfig{
|
||||
CheckForUpdate: "u",
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
+34
-2
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+27
-16
@@ -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)
|
||||
}
|
||||
|
||||
@@ -502,7 +513,7 @@ func (gui *Gui) handleNewBranchOffCurrentItem() error {
|
||||
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)
|
||||
})
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
@@ -19,7 +20,9 @@ func (gui *Gui) handleCommitConfirm() error {
|
||||
flags = "--no-verify"
|
||||
}
|
||||
|
||||
return gui.withGpgHandling(gui.GitCommand.CommitCmdStr(message, flags), gui.Tr.CommittingStatus, func() error {
|
||||
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
|
||||
@@ -34,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),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
+45
-15
@@ -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,7 +260,7 @@ 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)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
+30
-315
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+41
-30
@@ -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,7 +450,9 @@ func (gui *Gui) handleAmendCommitPress() error {
|
||||
title: strings.Title(gui.Tr.AmendLastCommit),
|
||||
prompt: gui.Tr.SureToAmend,
|
||||
handleConfirm: func() error {
|
||||
return gui.withGpgHandling(gui.GitCommand.AmendHeadCmdStr(), gui.Tr.AmendingStatus, nil)
|
||||
cmdStr := gui.GitCommand.AmendHeadCmdStr()
|
||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(cmdStr, gui.Tr.Spans.AmendCommit, true))
|
||||
return gui.withGpgHandling(cmdStr, gui.Tr.AmendingStatus, nil)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -465,7 +469,7 @@ func (gui *Gui) handleCommitEditorPress() error {
|
||||
}
|
||||
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(
|
||||
gui.OSCommand.PrepareSubProcess("git", "commit"),
|
||||
gui.OSCommand.WithSpan(gui.Tr.Spans.Commit).PrepareSubProcess("git", "commit"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -476,7 +480,7 @@ func (gui *Gui) editFile(filename string) error {
|
||||
}
|
||||
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(
|
||||
gui.OSCommand.PrepareShellSubProcess(cmdStr),
|
||||
gui.OSCommand.WithSpan(gui.Tr.Spans.EditFile).PrepareShellSubProcess(cmdStr),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -604,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
|
||||
@@ -619,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})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -634,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 {
|
||||
@@ -654,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
|
||||
@@ -663,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,
|
||||
@@ -677,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))
|
||||
@@ -696,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 {
|
||||
@@ -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,6 +814,7 @@ func (gui *Gui) handleCustomCommand() error {
|
||||
return gui.prompt(promptOpts{
|
||||
title: gui.Tr.CustomCommand,
|
||||
handleConfirm: func(command string) error {
|
||||
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)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -97,10 +97,35 @@ 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}
|
||||
|
||||
@@ -236,33 +236,59 @@ func TestBuildFlatTreeFromFiles(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "paths that can be sorted including a merge conflict file",
|
||||
name: "tracked, untracked, and conflicted files",
|
||||
files: []*models.File{
|
||||
{
|
||||
Name: "z",
|
||||
Name: "a2",
|
||||
Tracked: false,
|
||||
},
|
||||
{
|
||||
Name: "a1",
|
||||
Tracked: false,
|
||||
},
|
||||
{
|
||||
Name: "c2",
|
||||
HasMergeConflicts: true,
|
||||
},
|
||||
{
|
||||
Name: "b",
|
||||
Name: "c1",
|
||||
HasMergeConflicts: true,
|
||||
},
|
||||
{
|
||||
Name: "a",
|
||||
Name: "b2",
|
||||
Tracked: true,
|
||||
},
|
||||
{
|
||||
Name: "b1",
|
||||
Tracked: true,
|
||||
},
|
||||
},
|
||||
expected: &FileNode{
|
||||
Path: "",
|
||||
Children: []*FileNode{
|
||||
{
|
||||
File: &models.File{Name: "z", HasMergeConflicts: true},
|
||||
Path: "z",
|
||||
File: &models.File{Name: "c1", HasMergeConflicts: true},
|
||||
Path: "c1",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "a"},
|
||||
Path: "a",
|
||||
File: &models.File{Name: "c2", HasMergeConflicts: true},
|
||||
Path: "c2",
|
||||
},
|
||||
{
|
||||
File: &models.File{Name: "b"},
|
||||
Path: "b",
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+2
-2
@@ -32,7 +32,7 @@ func (gui *Gui) gitFlowFinishBranch(gitFlowConfig string, branchName string) err
|
||||
}
|
||||
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(
|
||||
gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "finish", suffix),
|
||||
gui.OSCommand.WithSpan(gui.Tr.Spans.GitFlowFinish).PrepareSubProcess("git", "flow", branchType, "finish", suffix),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ func (gui *Gui) handleCreateGitFlowMenu() error {
|
||||
title: title,
|
||||
handleConfirm: func(name string) error {
|
||||
return gui.runSubprocessWithSuspenseAndRefresh(
|
||||
gui.OSCommand.PrepareSubProcess("git", "flow", branchType, "start", name),
|
||||
gui.OSCommand.WithSpan(gui.Tr.Spans.GitFlowStart).PrepareSubProcess("git", "flow", branchType, "start", name),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
+38
-51
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+33
-29
@@ -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 = 4
|
||||
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
|
||||
@@ -109,6 +113,13 @@ type Gui struct {
|
||||
// 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 {
|
||||
@@ -126,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.
|
||||
@@ -223,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
|
||||
@@ -250,6 +252,7 @@ type Views struct {
|
||||
SearchPrefix *gocui.View
|
||||
Limit *gocui.View
|
||||
Suggestions *gocui.View
|
||||
Extras *gocui.View
|
||||
}
|
||||
|
||||
type searchingState struct {
|
||||
@@ -333,7 +336,6 @@ type guiState struct {
|
||||
IsRefreshingFiles bool
|
||||
Searching searchingState
|
||||
ScreenMode WindowMaximisation
|
||||
SideView *gocui.View
|
||||
Ptmx *os.File
|
||||
PrevMainWidth int
|
||||
PrevMainHeight int
|
||||
@@ -415,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{
|
||||
@@ -459,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
|
||||
}
|
||||
|
||||
@@ -713,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,
|
||||
@@ -721,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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+70
-4
@@ -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
|
||||
}
|
||||
@@ -1111,6 +1114,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,
|
||||
@@ -1702,6 +1712,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"} {
|
||||
@@ -1710,8 +1776,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},
|
||||
}...)
|
||||
}
|
||||
|
||||
|
||||
+9
-17
@@ -1,28 +1,11 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
func (gui *Gui) createAllViews() error {
|
||||
viewNameMappings := []struct {
|
||||
@@ -48,6 +31,7 @@ func (gui *Gui) createAllViews() error {
|
||||
{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
|
||||
@@ -134,6 +118,12 @@ func (gui *Gui) createAllViews() error {
|
||||
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
|
||||
}
|
||||
@@ -235,6 +225,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
||||
{viewName: "search", windowName: "search", frame: false},
|
||||
{viewName: "appStatus", windowName: "appStatus", frame: false},
|
||||
{viewName: "information", windowName: "information", frame: false},
|
||||
{viewName: "extras", windowName: "extras", frame: true},
|
||||
}
|
||||
|
||||
for _, arg := range args {
|
||||
@@ -344,6 +335,7 @@ func (gui *Gui) onInitialViewsCreation() error {
|
||||
gui.Views.CommitFiles,
|
||||
gui.Views.Main,
|
||||
gui.Views.Secondary,
|
||||
gui.Views.Extras,
|
||||
|
||||
// bottom line
|
||||
gui.Views.Options,
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
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)
|
||||
}
|
||||
+68
-175
@@ -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,82 +16,38 @@ 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.setViewContent(gui.Views.Secondary, secondaryPatchParser.Render(-1, -1, nil))
|
||||
|
||||
@@ -98,107 +55,66 @@ func (gui *Gui) refreshLineByLinePanel(diff string, secondaryDiff string, second
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -208,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
|
||||
@@ -220,7 +136,7 @@ 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
|
||||
@@ -232,21 +148,14 @@ func (gui *Gui) refreshMainViewForLineByLine(state *lBlPanelState) error {
|
||||
|
||||
// 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
|
||||
|
||||
if includeCurrentHunk {
|
||||
hunk := state.PatchParser.GetHunkContainingLine(state.SelectedLineIdx, 0)
|
||||
firstLineIdx = hunk.FirstLineIdx
|
||||
lastLineIdx = hunk.LastLineIdx()
|
||||
}
|
||||
firstLineIdx, lastLineIdx := state.SelectedRange()
|
||||
|
||||
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
|
||||
|
||||
@@ -264,41 +173,25 @@ func (gui *Gui) focusSelection(includeCurrentHunk bool, state *lBlPanelState) er
|
||||
return err
|
||||
}
|
||||
|
||||
return stagingView.SetCursor(0, state.SelectedLineIdx-newOrigin)
|
||||
return stagingView.SetCursor(0, state.GetSelectedLineIdx()-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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -307,7 +200,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 {
|
||||
@@ -324,8 +217,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
|
||||
@@ -336,48 +228,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()
|
||||
|
||||
|
||||
+2
-404
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+67
-91
@@ -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,21 +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()
|
||||
newOriginY := int(math.Max(0, float64(conflict.Middle-(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
|
||||
|
||||
@@ -263,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
|
||||
}
|
||||
@@ -330,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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package mergeconflicts
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"strings"
|
||||
"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 bars 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 findConflicts(content string) []*mergeConflict {
|
||||
conflicts := make([]*mergeConflict, 0)
|
||||
|
||||
if content == "" {
|
||||
return conflicts
|
||||
}
|
||||
|
||||
var newConflict *mergeConflict
|
||||
for i, line := range utils.SplitLines(content) {
|
||||
trimmedLine := strings.TrimPrefix(line, "++")
|
||||
switch trimmedLine {
|
||||
case "<<<<<<< HEAD", "<<<<<<< MERGE_HEAD", "<<<<<<< Updated upstream", "<<<<<<< ours":
|
||||
newConflict = &mergeConflict{start: i}
|
||||
case "=======":
|
||||
newConflict.middle = i
|
||||
default:
|
||||
if strings.HasPrefix(trimmedLine, ">>>>>>> ") {
|
||||
newConflict.end = i
|
||||
conflicts = append(conflicts, newConflict)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return conflicts
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
reader := bufio.NewReader(file)
|
||||
content := ""
|
||||
for i := 0; true; i++ {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if !isIndexToDelete(i, conflict, selection) {
|
||||
content += line
|
||||
}
|
||||
}
|
||||
|
||||
return true, content, nil
|
||||
}
|
||||
|
||||
func isIndexToDelete(i int, conflict *mergeConflict, 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)
|
||||
}
|
||||
@@ -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.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
|
||||
}
|
||||
|
||||
+19
-18
@@ -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}})
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/fatih/color"
|
||||
"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"
|
||||
)
|
||||
@@ -106,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 {
|
||||
|
||||
+14
-12
@@ -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
|
||||
|
||||
+19
-7
@@ -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
|
||||
|
||||
@@ -21,7 +21,7 @@ func (gui *Gui) handleCreateResetMenu() error {
|
||||
red.Sprint(nukeStr),
|
||||
},
|
||||
onPress: func() error {
|
||||
if err := gui.GitCommand.ResetAndClean(); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.NukeWorkingTree).ResetAndClean(); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ func (gui *Gui) handleCreateResetMenu() error {
|
||||
red.Sprint("git checkout -- ."),
|
||||
},
|
||||
onPress: func() error {
|
||||
if err := gui.GitCommand.DiscardAnyUnstagedFileChanges(); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.DiscardUnstagedFileChanges).DiscardAnyUnstagedFileChanges(); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ func (gui *Gui) handleCreateResetMenu() error {
|
||||
red.Sprint("git clean -fd"),
|
||||
},
|
||||
onPress: func() error {
|
||||
if err := gui.GitCommand.RemoveUntrackedFiles(); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.RemoveUntrackedFiles).RemoveUntrackedFiles(); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ func (gui *Gui) handleCreateResetMenu() error {
|
||||
red.Sprint("git reset --soft HEAD"),
|
||||
},
|
||||
onPress: func() error {
|
||||
if err := gui.GitCommand.ResetSoft("HEAD"); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.SoftReset).ResetSoft("HEAD"); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ func (gui *Gui) handleCreateResetMenu() error {
|
||||
red.Sprint("git reset --mixed HEAD"),
|
||||
},
|
||||
onPress: func() error {
|
||||
if err := gui.GitCommand.ResetSoft("HEAD"); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.MixedReset).ResetMixed("HEAD"); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ func (gui *Gui) handleCreateResetMenu() error {
|
||||
red.Sprint("git reset --hard HEAD"),
|
||||
},
|
||||
onPress: func() error {
|
||||
if err := gui.GitCommand.ResetHard("HEAD"); err != nil {
|
||||
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.HardReset).ResetHard("HEAD"); err != nil {
|
||||
return gui.surfaceError(err)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -207,7 +207,7 @@ func dutchTranslationSet() TranslationSet {
|
||||
ConfirmMerge: "Weet je zeker dat je {{.selectedBranch}} in {{.checkedOutBranch}} wil mergen?",
|
||||
FwdNoUpstream: "Kan niet de branch vooruitspoelen zonder upstream",
|
||||
FwdCommitsToPush: "Je kan niet vooruitspoelen als de branch geen nieuwe commits heeft",
|
||||
ErrorOccurred: "Er is iets fout gegaan! Zou je hier een issue aan willen maken: https://github.com/jesseduffield/lazygit/issues",
|
||||
ErrorOccurred: "Er is iets fout gegaan! Zou je hier een issue aan willen maken",
|
||||
NoRoom: "Niet genoeg ruimte",
|
||||
YouAreHere: "JE BENT HIER",
|
||||
LcRewordNotSupported: "herformatteren van commits in interactief rebasen is nog niet ondersteund",
|
||||
|
||||
+186
-208
@@ -11,7 +11,6 @@ Todo list when making a new translation
|
||||
package i18n
|
||||
|
||||
type TranslationSet struct {
|
||||
ReleaseNotes string
|
||||
NotEnoughSpace string
|
||||
DiffTitle string
|
||||
LogTitle string
|
||||
@@ -233,6 +232,7 @@ type TranslationSet struct {
|
||||
CannotRebaseOntoFirstCommit string
|
||||
CannotSquashOntoSecondCommit string
|
||||
Donate string
|
||||
AskQuestion string
|
||||
PrevLine string
|
||||
NextLine string
|
||||
PrevHunk string
|
||||
@@ -386,6 +386,7 @@ type TranslationSet struct {
|
||||
DiffingMenuTitle string
|
||||
LcSwapDiff string
|
||||
LcOpenDiffingMenu string
|
||||
LcOpenExtrasMenu string
|
||||
LcShowingGitDiff string
|
||||
LcCopyCommitShaToClipboard string
|
||||
LcCopyCommitMessageToClipboard string
|
||||
@@ -443,226 +444,114 @@ type TranslationSet struct {
|
||||
ErrCannotEditDirectory string
|
||||
ErrStageDirWithInlineMergeConflicts string
|
||||
ErrRepositoryMovedOrDeleted string
|
||||
CommandLog string
|
||||
ToggleShowCommandLog string
|
||||
FocusCommandLog string
|
||||
CommandLogHeader string
|
||||
RandomTip string
|
||||
Spans Spans
|
||||
}
|
||||
|
||||
const englishReleaseNotes = `lazygit 0.27.1-0.27.4 Release notes
|
||||
|
||||
Version 0.27 was quite ambitious so a few bugs were introduced. Lesson learnt,
|
||||
release features gradually! All the newly introduced bugs should now be fixed,
|
||||
except for https://github.com/jesseduffield/lazygit/issues/1233 which looks to
|
||||
be a limitation of the new rendering library, tcell.
|
||||
|
||||
You can also now open 'git mergetool' by pressing shift+M in the files panel or
|
||||
merge panel.
|
||||
|
||||
Otherwise, these releases contained minor bug fixes and improvements.
|
||||
|
||||
lazygit 0.27 Release Notes
|
||||
|
||||
Holy Moly, this is a big one.
|
||||
|
||||
There are two big changes here:
|
||||
1) Tree view for the files panel
|
||||
2) New rendering library
|
||||
|
||||
## File tree view
|
||||
|
||||
This is off by default, but can be configured via the 'gui.showFileTree' config
|
||||
key and toggled from within lazygit with the backtick key (the one below tilde).
|
||||
|
||||
Hitting enter on directories will toggle whether they are collapsed. Most
|
||||
keybindings that apply to files also apply to directories e.g. if you hit space
|
||||
on a directory, it will stage that whole directory.
|
||||
|
||||
When not in tree-mode, the merge conflicts are now bubbled up to the top of the
|
||||
list.
|
||||
|
||||
The tree view makes it much easier to deal with tonnes of files, because you can
|
||||
easily collapse folders you don't care about to focus on the important changes.
|
||||
It also reduces the amount of horizontal space used meaning there is less chance
|
||||
of content being truncated by the frame of the panel.
|
||||
|
||||
## New rendering library
|
||||
|
||||
We've switched from the termbox package to tcell, with the help of the contributors
|
||||
of the awesome-gocui repo. This has many benefits:
|
||||
- More support for various terminals
|
||||
- 24 bit colour support (you can now drop the -24-bit-color=never arg if you're using delta)
|
||||
- Support for more keybindings like... SHIFT-TAB! Which means you can now navigate
|
||||
the side panels with tab and shift-tab. (Previously pressing shift+tab would
|
||||
crash the program).
|
||||
- Better support for switching to subprocesses. Most of that benefit
|
||||
|
||||
Other stuff:
|
||||
- No more flickering e.g. when staging a file or when contents are refreshed
|
||||
- You can now scroll the main panel with your mouse or pgup/pgdown. Before, doing
|
||||
so would move the cursor which was weird
|
||||
- You can now insert a newline to the commit message panel via alt-enter. I've
|
||||
changed the default keybinding from <tab> to <a-enter>. Let me know if that makes
|
||||
you angry
|
||||
- When you scroll the main view, it will now stop just shy of scrolling too far
|
||||
- The gui no longer re-initialises when returning from a subprocess or switching
|
||||
repos
|
||||
- By default, 'esc' no longer quits lazygit. Instead you'll need to use ctrl+c
|
||||
or 'q'. We use escape for exiting various modes in lazygit (e.g. cherry-picking)
|
||||
and it gets annoying when you accidentally hit esc one too many times and end
|
||||
up quitting. It's still configurable though
|
||||
- Faster startup time
|
||||
- Custom commands now run in your shell so you have more freedom to get freaky with it
|
||||
|
||||
Bug fixes:
|
||||
- No more panicking when attemping to enter an unprintable key (thanks @fsmiamoto!)
|
||||
- Rewording the topmost commit no longer commits staged files as well
|
||||
- When returning from a submodule we retain the state of the parent repo so that
|
||||
you land back where you were in the submodules tab
|
||||
- Fixed a bug in search where the cursor would get stuck if the result set shrunk
|
||||
- Commands now retry if .git/index.lock exists
|
||||
- Branches are no longer checked out when renamed
|
||||
- Fixed issue with merge conflicts on windows where the wrong command was invoked
|
||||
causing a panic
|
||||
|
||||
Code-stuff:
|
||||
- Lots of refactoring of the code itself. I'm considering a much bigger refactor
|
||||
but need to investigate whether the approach is a good idea
|
||||
- Added a TUI for running/recording integration tests so that that whole workflow
|
||||
is easier
|
||||
- Added over 40 new integration tests, so bugs will be caught sooner. As always,
|
||||
if you catch a bug, please raise an issue for it!
|
||||
|
||||
|
||||
## lazygit 0.26 Release Notes
|
||||
|
||||
- Config changes applied after editing from within lazygit, no reload required.
|
||||
|
||||
- LOTS of fixes for rendering filenames with strange characters, escaped
|
||||
characters, and UI fixes, by the amazing @Ryooooooga!
|
||||
|
||||
- Also thanks to @Isti115
|
||||
|
||||
## lazygit 0.25 Release Notes
|
||||
|
||||
- Fixes for windows, thanks @murphy66!
|
||||
|
||||
- Allow mapping spaces to dashes when creating a branch, thanks @caquillo07!
|
||||
|
||||
- Allow configuring file refresh and fetch frequency, thanks @Liberatys!
|
||||
|
||||
- Minor security improvement
|
||||
|
||||
- Wide characters supported when entering commit messages, thanks @Ryooooooga!
|
||||
|
||||
- Original branch name appears when renaming, thanks piresrui!
|
||||
|
||||
- Better menus, thanks @1jz!
|
||||
|
||||
- Also thanks to @snipem, @dbast, and @dawidd6
|
||||
|
||||
## lazygit 0.24 Release Notes
|
||||
|
||||
- Suggestions now shown when checking out branch by name
|
||||
|
||||
- Minimum OSX version is now officially 10.10
|
||||
|
||||
- Pull requests URLs can be copied from the keyboard, thanks @farzadmf!
|
||||
|
||||
- Allow --follow-tags flag for git push to be disabled in config,
|
||||
thanks @fishybell!
|
||||
|
||||
- Allow quick commit when no files are staged and the user presses 'c',
|
||||
thanks @fluffynuts!
|
||||
|
||||
- Lazygit config is now by default created with 'jesseduffield' as the parent
|
||||
folder, thanks @Liberatys!
|
||||
|
||||
- You can now configure how lazygit behaves when you open it outside a repo
|
||||
(e.g. skip the prompt and open the most recent repo), thanks @kalvinpearce!
|
||||
|
||||
- You can now visualise the commit graph for all branches by pressing 'a' in
|
||||
the status panel - thanks @Yuuki77!
|
||||
|
||||
- And thanks to @dawidd6, @sstiglitz, @fargozhu and @nils-a for helping out with
|
||||
CI and documentation!
|
||||
|
||||
## lazygit 0.23.2 Release Notes
|
||||
|
||||
- Fixed bug where editing a file with spaces did not work
|
||||
- Fixed formatting issue with delta that rendered '[0;K' to the screen
|
||||
- Fixed bug where lazygit failed upon attempting to create a config file in a
|
||||
read-only filesystem
|
||||
|
||||
## lazygit 0.23 Release Notes
|
||||
|
||||
Custom Commands:
|
||||
- You can now create your own custom commands complete with menus and prompts
|
||||
to tailor lazygit to your workflow
|
||||
|
||||
See https://github.com/jesseduffield/lazygit/wiki/Custom-Commands-Compendium
|
||||
and https://github.com/jesseduffield/lazygit/blob/master/docs/Custom_Command_Keybindings.md
|
||||
|
||||
Submodules:
|
||||
- Add, update, and sync submodules with the new submodules tab. To enter a
|
||||
submodule hit enter on it and then hit escape to return to the superproject
|
||||
|
||||
Bare repos:
|
||||
- Bare repos are now supported with the --git-dir and --work-tree args, so you
|
||||
can use lazygit to manage your dotfiles!
|
||||
|
||||
Staging panel navigation:
|
||||
- Ability to search with '/' and jump page by page with ',', '.', '<', '>' in
|
||||
the staging and patch-building contexts
|
||||
|
||||
More clipboard stuff:
|
||||
- More text copying. Pressing ` + "`" + `ctrl+o` + "`" + ` on a commit to copy
|
||||
its SHA, or a file to copy its name, etc.
|
||||
|
||||
Easily view lazygit logs:
|
||||
- View lazygit logs with ` + "`" + `lazygit --logs` + "`" + ` (in another
|
||||
terminal tab run ` + "`" + `lazygit --debug` + "`" + ` to write to logs)
|
||||
|
||||
Other:
|
||||
- For the butterfingers of the world, you are now protected from accidentally
|
||||
deleting the .gitignore file (thanks @kobutomo!)
|
||||
|
||||
- Fewer panics
|
||||
|
||||
- No more 'invalid merge' errors on startup
|
||||
|
||||
- Smaller binary after ditching the Viper and i18n package. Beware! This means
|
||||
configs are now case-sensitive so if your config stops working check the case
|
||||
sensitivity of the keys against what you get from ` + "`" + `lazygit --config` + "`" + `
|
||||
|
||||
- Code refactor for better dev experience including more type safety
|
||||
|
||||
- Integration tests have finally been added and there are many more to come.
|
||||
These will assist in ensuring no regressions have been introduced in future
|
||||
releases. Making an integration test is actually pretty fun you basically just
|
||||
record yourself using lazygit and that's it. See the guide to integration tests at
|
||||
https://github.com/jesseduffield/lazygit/blob/master/docs/Integration_Tests.md
|
||||
|
||||
- Showing release notes from within lazygit, as you no doubt have realised. I'm
|
||||
too lazy to include retrospective release notes but better late than never.`
|
||||
type Spans struct {
|
||||
CheckoutCommit string
|
||||
CheckoutReflogCommit string
|
||||
CheckoutTag string
|
||||
CheckoutBranch string
|
||||
ForceCheckoutBranch string
|
||||
DeleteBranch string
|
||||
Merge string
|
||||
RebaseBranch string
|
||||
RenameBranch string
|
||||
CreateBranch string
|
||||
FastForwardBranch string
|
||||
CherryPick string
|
||||
CheckoutFile string
|
||||
DiscardOldFileChange string
|
||||
SquashCommitDown string
|
||||
FixupCommit string
|
||||
RewordCommit string
|
||||
DropCommit string
|
||||
EditCommit string
|
||||
AmendCommit string
|
||||
RevertCommit string
|
||||
CreateFixupCommit string
|
||||
SquashAllAboveFixupCommits string
|
||||
MoveCommitUp string
|
||||
MoveCommitDown string
|
||||
CopyCommitMessageToClipboard string
|
||||
CustomCommand string
|
||||
DiscardAllChangesInDirectory string
|
||||
DiscardUnstagedChangesInDirectory string
|
||||
DiscardAllChangesInFile string
|
||||
DiscardAllUnstagedChangesInFile string
|
||||
StageFile string
|
||||
UnstageFile string
|
||||
UnstageAllFiles string
|
||||
StageAllFiles string
|
||||
IgnoreFile string
|
||||
Commit string
|
||||
EditFile string
|
||||
Push string
|
||||
Pull string
|
||||
OpenFile string
|
||||
StashAllChanges string
|
||||
StashStagedChanges string
|
||||
GitFlowFinish string
|
||||
GitFlowStart string
|
||||
CopyToClipboard string
|
||||
RemovePatchFromCommit string
|
||||
MovePatchToSelectedCommit string
|
||||
MovePatchIntoIndex string
|
||||
MovePatchIntoNewCommit string
|
||||
DeleteRemoteBranch string
|
||||
SetBranchUpstream string
|
||||
AddRemote string
|
||||
RemoveRemote string
|
||||
UpdateRemote string
|
||||
ApplyPatch string
|
||||
Stash string
|
||||
RemoveSubmodule string
|
||||
ResetSubmodule string
|
||||
AddSubmodule string
|
||||
UpdateSubmoduleUrl string
|
||||
InitialiseSubmodule string
|
||||
BulkInitialiseSubmodules string
|
||||
BulkUpdateSubmodules string
|
||||
BulkStashAndResetSubmodules string
|
||||
BulkDeinitialiseSubmodules string
|
||||
UpdateSubmodule string
|
||||
CreateLightweightTag string
|
||||
DeleteTag string
|
||||
PushTag string
|
||||
NukeWorkingTree string
|
||||
DiscardUnstagedFileChanges string
|
||||
RemoveUntrackedFiles string
|
||||
SoftReset string
|
||||
MixedReset string
|
||||
HardReset string
|
||||
Undo string
|
||||
Redo string
|
||||
}
|
||||
|
||||
const englishIntroPopupMessage = `
|
||||
Thanks for using lazygit! Three things to share with you:
|
||||
Thanks for using lazygit! Seriously you rock. Three things to share with you:
|
||||
|
||||
1) If you want to learn about lazygit's features, watch this vid:
|
||||
https://youtu.be/CPLdltN7wgE
|
||||
|
||||
2) If you're using git, that makes you a programmer! With your help we can make
|
||||
3) Be sure to read the latest release notes at:
|
||||
https://github.com/jesseduffield/lazygit/releases
|
||||
|
||||
3) If you're using git, that makes you a programmer! With your help we can make
|
||||
lazygit better, so consider becoming a contributor and joining the fun at
|
||||
https://github.com/jesseduffield/lazygit
|
||||
You can also sponsor me and tell me what to work on by clicking the donate
|
||||
button at the bottom right (github is still matching donations dollar-for-dollar.)
|
||||
button at the bottom right.
|
||||
Or even just star the repo cos we're not far from 20k stars!
|
||||
|
||||
3) You can now read through the release notes by navigating to the status panel.
|
||||
Version 0.27 introduces a file tree view, better rendering, tab/shift+tab
|
||||
keybindings, and more
|
||||
`
|
||||
|
||||
func englishTranslationSet() TranslationSet {
|
||||
return TranslationSet{
|
||||
ReleaseNotes: englishReleaseNotes,
|
||||
NotEnoughSpace: "Not enough space to render panels",
|
||||
DiffTitle: "Diff",
|
||||
LogTitle: "Log",
|
||||
@@ -872,7 +761,7 @@ func englishTranslationSet() TranslationSet {
|
||||
ConfirmMerge: "Are you sure you want to merge {{.selectedBranch}} into {{.checkedOutBranch}}?",
|
||||
FwdNoUpstream: "Cannot fast-forward a branch with no upstream",
|
||||
FwdCommitsToPush: "Cannot fast-forward a branch with commits to push",
|
||||
ErrorOccurred: "An error occurred! Please create an issue at https://github.com/jesseduffield/lazygit/issues",
|
||||
ErrorOccurred: "An error occurred! Please create an issue at",
|
||||
NoRoom: "Not enough room",
|
||||
YouAreHere: "YOU ARE HERE",
|
||||
LcRewordNotSupported: "rewording commits while interactively rebasing is not currently supported",
|
||||
@@ -884,6 +773,7 @@ func englishTranslationSet() TranslationSet {
|
||||
CannotRebaseOntoFirstCommit: "You cannot interactive rebase onto the first commit",
|
||||
CannotSquashOntoSecondCommit: "You cannot squash/fixup onto the second commit",
|
||||
Donate: "Donate",
|
||||
AskQuestion: "Ask Question",
|
||||
PrevLine: "select previous line",
|
||||
NextLine: "select next line",
|
||||
PrevHunk: "select previous hunk",
|
||||
@@ -936,8 +826,8 @@ func englishTranslationSet() TranslationSet {
|
||||
LcHardResetUpstream: "hard reset to upstream branch",
|
||||
LcViewResetOptions: `view reset options`,
|
||||
LcCreateFixupCommit: `create fixup commit for this commit`,
|
||||
LcSquashAboveCommits: `squash above commits`,
|
||||
SquashAboveCommits: `Squash above commits`,
|
||||
LcSquashAboveCommits: `squash all 'fixup!' commits above selected commit (autosquash)`,
|
||||
SquashAboveCommits: `Squash all 'fixup!' commits above selected commit (autosquash)`,
|
||||
SureSquashAboveCommits: `Are you sure you want to squash all fixup! commits above {{.commit}}?`,
|
||||
CreateFixupCommit: `Create fixup commit`,
|
||||
SureCreateFixupCommit: `Are you sure you want to create a fixup! commit for commit {{.commit}}?`,
|
||||
@@ -1037,6 +927,8 @@ func englishTranslationSet() TranslationSet {
|
||||
DiffingMenuTitle: "Diffing",
|
||||
LcSwapDiff: "reverse diff direction",
|
||||
LcOpenDiffingMenu: "open diff menu",
|
||||
// the actual view is the extras view which I intend to give more tabs in future but for now we'll only mention the command log part
|
||||
LcOpenExtrasMenu: "open command log menu",
|
||||
LcShowingGitDiff: "showing output for:",
|
||||
LcCopyCommitShaToClipboard: "copy commit SHA to clipboard",
|
||||
LcCopyCommitMessageToClipboard: "copy commit message to clipboard",
|
||||
@@ -1094,5 +986,91 @@ func englishTranslationSet() TranslationSet {
|
||||
ErrCannotEditDirectory: "Cannot edit directory: you can only edit individual files",
|
||||
ErrStageDirWithInlineMergeConflicts: "Cannot stage/unstage directory containing files with inline merge conflicts. Please fix up the merge conflicts first",
|
||||
ErrRepositoryMovedOrDeleted: "Cannot find repo. It might have been moved or deleted ¯\\_(ツ)_/¯",
|
||||
CommandLog: "Command Log",
|
||||
ToggleShowCommandLog: "Toggle show/hide command log",
|
||||
FocusCommandLog: "Focus command log",
|
||||
CommandLogHeader: "You can hide/focus this panel by pressing '%s' or hide it permanently in your config with `gui.showCommandLog: false`\n",
|
||||
RandomTip: "Random Tip",
|
||||
Spans: Spans{
|
||||
// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm)
|
||||
CheckoutCommit: "Checkout commit",
|
||||
CheckoutReflogCommit: "Checkout reflog commit",
|
||||
CheckoutTag: "Checkout tag",
|
||||
CheckoutBranch: "Checkout branch",
|
||||
ForceCheckoutBranch: "Force checkout branch",
|
||||
DeleteBranch: "Delete branch",
|
||||
Merge: "Merge",
|
||||
RebaseBranch: "Rebase branch",
|
||||
RenameBranch: "Rename branch",
|
||||
CreateBranch: "Create branch",
|
||||
CherryPick: "(Cherry-pick) Paste commits",
|
||||
CheckoutFile: "Checkout file",
|
||||
DiscardOldFileChange: "Discard old file change",
|
||||
SquashCommitDown: "Squash commit down",
|
||||
FixupCommit: "Fixup commit",
|
||||
RewordCommit: "Reword commit",
|
||||
DropCommit: "Drop commit",
|
||||
EditCommit: "Edit commit",
|
||||
AmendCommit: "Amend commit",
|
||||
RevertCommit: "Revert commit",
|
||||
CreateFixupCommit: "Create fixup commit",
|
||||
SquashAllAboveFixupCommits: "Squash all above fixup commits",
|
||||
CreateLightweightTag: "Create lightweight tag",
|
||||
CopyCommitMessageToClipboard: "Copy commit message to clipboard",
|
||||
MoveCommitUp: "Move commit up",
|
||||
MoveCommitDown: "Move commit down",
|
||||
CustomCommand: "Custom command",
|
||||
DiscardAllChangesInDirectory: "Discard all changes in directory",
|
||||
DiscardUnstagedChangesInDirectory: "Discard unstaged changes in directory",
|
||||
DiscardAllChangesInFile: "Discard all changes in file",
|
||||
DiscardAllUnstagedChangesInFile: "Discard all unstaged changes in file",
|
||||
StageFile: "Stage file",
|
||||
UnstageFile: "Unstage file",
|
||||
UnstageAllFiles: "Unstage all files",
|
||||
StageAllFiles: "Stage all files",
|
||||
IgnoreFile: "Ignore file",
|
||||
Commit: "Commit",
|
||||
EditFile: "Edit file",
|
||||
Push: "Push",
|
||||
Pull: "Pull",
|
||||
OpenFile: "Open file",
|
||||
StashAllChanges: "Stash all changes",
|
||||
StashStagedChanges: "Stash staged changes",
|
||||
GitFlowFinish: "Git flow finish",
|
||||
GitFlowStart: "Git Flow start",
|
||||
CopyToClipboard: "Copy to clipboard",
|
||||
RemovePatchFromCommit: "Remove patch from commit",
|
||||
MovePatchToSelectedCommit: "Move patch to selected commit",
|
||||
MovePatchIntoIndex: "Move patch into index",
|
||||
MovePatchIntoNewCommit: "Move patch into new commit",
|
||||
DeleteRemoteBranch: "Delete remote branch",
|
||||
SetBranchUpstream: "Set branch upstream",
|
||||
AddRemote: "Add remote",
|
||||
RemoveRemote: "Remove remote",
|
||||
UpdateRemote: "Update remote",
|
||||
ApplyPatch: "Apply patch",
|
||||
Stash: "Stash",
|
||||
RemoveSubmodule: "Remove submodule",
|
||||
ResetSubmodule: "Reset submodule",
|
||||
AddSubmodule: "Add submodule",
|
||||
UpdateSubmoduleUrl: "Update submodule URL",
|
||||
InitialiseSubmodule: "Initialise submodule",
|
||||
BulkInitialiseSubmodules: "Bulk initialise submodules",
|
||||
BulkUpdateSubmodules: "Bulk update submodules",
|
||||
BulkStashAndResetSubmodules: "Bulk stash and reset submodules",
|
||||
BulkDeinitialiseSubmodules: "Bulk deinitialise submodules",
|
||||
UpdateSubmodule: "Update submodule",
|
||||
DeleteTag: "Delete tag",
|
||||
PushTag: "Push tag",
|
||||
NukeWorkingTree: "Nuke working tree",
|
||||
DiscardUnstagedFileChanges: "Discard unstaged file changes",
|
||||
RemoveUntrackedFiles: "Remove untracked files",
|
||||
SoftReset: "Soft reset",
|
||||
MixedReset: "Mixed reset",
|
||||
HardReset: "Hard reset",
|
||||
FastForwardBranch: "Fast forward branch",
|
||||
Undo: "Undo",
|
||||
Redo: "Redo",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -145,7 +145,7 @@ func polishTranslationSet() TranslationSet {
|
||||
ConfirmMerge: "Are you sure you want to merge {{.selectedBranch}} into {{.checkedOutBranch}}?",
|
||||
FwdNoUpstream: "Cannot fast-forward a branch with no upstream",
|
||||
FwdCommitsToPush: "Cannot fast-forward a branch with commits to push",
|
||||
ErrorOccurred: "An error occurred! Please create an issue at https://github.com/jesseduffield/lazygit/issues",
|
||||
ErrorOccurred: "An error occurred! Please create an issue at",
|
||||
MainTitle: "Main",
|
||||
NormalTitle: "Normal",
|
||||
LcSoftReset: "soft reset",
|
||||
@@ -225,8 +225,8 @@ func polishTranslationSet() TranslationSet {
|
||||
LcHardReset: "hard reset",
|
||||
LcViewResetOptions: `view reset options`,
|
||||
LcCreateFixupCommit: `create fixup commit for this commit`,
|
||||
LcSquashAboveCommits: `squash above commits`,
|
||||
SquashAboveCommits: `Squash above commits`,
|
||||
LcSquashAboveCommits: `squash all 'fixup!' commits above selected commits (autosquash)`,
|
||||
SquashAboveCommits: `Squash all 'fixup!' commits above selected commits (autosquash)`,
|
||||
SureSquashAboveCommits: `Are you sure you want to squash all fixup! commits above {{.commit}}?`,
|
||||
CreateFixupCommit: `Create fixup commit`,
|
||||
SureCreateFixupCommit: `Are you sure you want to create a fixup! commit for commit {{.commit}}?`,
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/constants"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -36,10 +37,6 @@ type Updaterer interface {
|
||||
Update()
|
||||
}
|
||||
|
||||
const (
|
||||
PROJECT_URL = "https://github.com/jesseduffield/lazygit"
|
||||
)
|
||||
|
||||
// NewUpdater creates a new updater
|
||||
func NewUpdater(log *logrus.Entry, config config.AppConfigurer, osCommand *oscommands.OSCommand, tr *i18n.TranslationSet) (*Updater, error) {
|
||||
contextLogger := log.WithField("context", "updates")
|
||||
@@ -53,7 +50,7 @@ func NewUpdater(log *logrus.Entry, config config.AppConfigurer, osCommand *oscom
|
||||
}
|
||||
|
||||
func (u *Updater) getLatestVersionNumber() (string, error) {
|
||||
req, err := http.NewRequest("GET", PROJECT_URL+"/releases/latest", nil)
|
||||
req, err := http.NewRequest("GET", constants.Links.RepoUrl+"/releases/latest", nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -235,7 +232,7 @@ func (u *Updater) zipExtension() string {
|
||||
func (u *Updater) getBinaryUrl(newVersion string) (string, error) {
|
||||
url := fmt.Sprintf(
|
||||
"%s/releases/download/%s/lazygit_%s_%s_%s.%s",
|
||||
PROJECT_URL,
|
||||
constants.Links.RepoUrl,
|
||||
newVersion,
|
||||
newVersion[1:],
|
||||
u.mappedOs(runtime.GOOS),
|
||||
|
||||
+25
-10
@@ -86,6 +86,24 @@ func (v *View) EditWrite(ch rune) {
|
||||
v.moveCursor(w, 0, true)
|
||||
}
|
||||
|
||||
func (v *View) EditWriteString(str string) {
|
||||
for _, ch := range str {
|
||||
v.EditWrite(ch)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *View) SetEditorContent(content string) error {
|
||||
v.Clear()
|
||||
if err := v.SetOrigin(0, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := v.SetCursor(0, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
v.EditWriteString(content)
|
||||
return nil
|
||||
}
|
||||
|
||||
// EditDeleteToStartOfLine is the equivalent of pressing ctrl+U in your terminal, it deletes to the start of the line. Or if you are already at the start of the line, it deletes the newline character
|
||||
func (v *View) EditDeleteToStartOfLine() {
|
||||
x, _ := v.Cursor()
|
||||
@@ -131,6 +149,9 @@ func (v *View) EditGotoToEndOfLine() {
|
||||
// EditDelete deletes a rune at the cursor position. back determines the
|
||||
// direction.
|
||||
func (v *View) EditDelete(back bool) {
|
||||
v.writeMutex.Lock()
|
||||
defer v.writeMutex.Unlock()
|
||||
|
||||
x, y := v.ox+v.cx, v.oy+v.cy
|
||||
if y < 0 {
|
||||
return
|
||||
@@ -177,6 +198,9 @@ func (v *View) EditDelete(back bool) {
|
||||
|
||||
// EditNewLine inserts a new line under the cursor.
|
||||
func (v *View) EditNewLine() {
|
||||
v.writeMutex.Lock()
|
||||
defer v.writeMutex.Unlock()
|
||||
|
||||
v.breakLine(v.cx, v.cy)
|
||||
v.ox = 0
|
||||
v.cy = v.cy + 1
|
||||
@@ -397,9 +421,6 @@ func (v *View) writeRune(x, y int, ch rune) error {
|
||||
// position corresponding to the point (x, y).
|
||||
// returns the amount of columns that where removed.
|
||||
func (v *View) deleteRune(x, y int) (int, error) {
|
||||
v.writeMutex.Lock()
|
||||
defer v.writeMutex.Unlock()
|
||||
|
||||
v.tainted = true
|
||||
|
||||
x, y, err := v.realPosition(x, y)
|
||||
@@ -427,10 +448,7 @@ func (v *View) deleteRune(x, y int) (int, error) {
|
||||
|
||||
// mergeLines merges the lines "y" and "y+1" if possible.
|
||||
func (v *View) mergeLines(y int) error {
|
||||
v.writeMutex.Lock()
|
||||
defer v.writeMutex.Unlock()
|
||||
|
||||
v.tainted = true
|
||||
v.clearViewLines()
|
||||
|
||||
_, y, err := v.realPosition(0, y)
|
||||
if err != nil {
|
||||
@@ -451,9 +469,6 @@ func (v *View) mergeLines(y int) error {
|
||||
// breakLine breaks a line of the internal buffer at the position corresponding
|
||||
// to the point (x, y).
|
||||
func (v *View) breakLine(x, y int) error {
|
||||
v.writeMutex.Lock()
|
||||
defer v.writeMutex.Unlock()
|
||||
|
||||
v.tainted = true
|
||||
|
||||
x, y, err := v.realPosition(x, y)
|
||||
|
||||
+10
-5
@@ -272,7 +272,7 @@ func (g *Gui) SetView(name string, x0, y0, x1, y1 int, overlaps byte) (*View, er
|
||||
|
||||
if v, err := g.View(name); err == nil {
|
||||
if v.x0 != x0 || v.x1 != x1 || v.y0 != y0 || v.y1 != y1 {
|
||||
v.tainted = true
|
||||
v.clearViewLines()
|
||||
}
|
||||
|
||||
v.x0 = x0
|
||||
@@ -651,14 +651,19 @@ func (g *Gui) handleEvent(ev *GocuiEvent) error {
|
||||
return g.onKey(ev)
|
||||
case eventError:
|
||||
return ev.Err
|
||||
// Not sure if this should be handled. It acts weirder when it's here
|
||||
// case eventResize:
|
||||
// return Sync()
|
||||
case eventResize:
|
||||
g.onResize()
|
||||
return nil
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Gui) onResize() {
|
||||
// not sure if we actually need this
|
||||
// g.screen.Sync()
|
||||
}
|
||||
|
||||
// flush updates the gui, re-drawing frames and buffers.
|
||||
func (g *Gui) flush() error {
|
||||
// pretty sure we don't need this, but keeping it here in case we get weird visual artifacts
|
||||
@@ -668,7 +673,7 @@ func (g *Gui) flush() error {
|
||||
// if GUI's size has changed, we need to redraw all views
|
||||
if maxX != g.maxX || maxY != g.maxY {
|
||||
for _, v := range g.views {
|
||||
v.tainted = true
|
||||
v.clearViewLines()
|
||||
}
|
||||
}
|
||||
g.maxX, g.maxY = maxX, maxY
|
||||
|
||||
+16
-6
@@ -48,7 +48,12 @@ type View struct {
|
||||
// tained is true if the viewLines must be updated
|
||||
tainted bool
|
||||
|
||||
// internal representation of the view's buffer
|
||||
// internal representation of the view's buffer. We will keep viewLines around
|
||||
// from a previous render until we explicitly set them to nil, allowing us to
|
||||
// render the same content twice without flicker. Wherever we want to render
|
||||
// something without any chance of old content appearing (e.g. when actually
|
||||
// rendering new content or if the view is resized) we should set tainted to
|
||||
// true and viewLines to nil
|
||||
viewLines []viewLine
|
||||
|
||||
// writeMutex protects locks the write process
|
||||
@@ -155,6 +160,14 @@ type View struct {
|
||||
KeybindOnEdit bool
|
||||
}
|
||||
|
||||
// call this in the event of a view resize, or if you want to render new content
|
||||
// without the chance of old content still appearing, or if you want to remove
|
||||
// a line from the existing content
|
||||
func (v *View) clearViewLines() {
|
||||
v.tainted = true
|
||||
v.viewLines = nil
|
||||
}
|
||||
|
||||
type searcher struct {
|
||||
searchString string
|
||||
searchPositions []cellPos
|
||||
@@ -651,9 +664,8 @@ func (v *View) Clear() {
|
||||
defer v.writeMutex.Unlock()
|
||||
|
||||
v.rewind()
|
||||
v.tainted = true
|
||||
v.lines = nil
|
||||
v.viewLines = nil
|
||||
v.clearViewLines()
|
||||
v.clearRunes()
|
||||
}
|
||||
|
||||
@@ -683,9 +695,7 @@ func (v *View) FlushStaleCells() {
|
||||
v.writeMutex.Lock()
|
||||
defer v.writeMutex.Unlock()
|
||||
|
||||
v.rewind()
|
||||
v.tainted = true
|
||||
v.viewLines = nil
|
||||
v.clearViewLines()
|
||||
}
|
||||
|
||||
func (v *View) rewind() {
|
||||
|
||||
+34
-5
@@ -106,6 +106,31 @@ func IoctlGetRTCTime(fd int) (*RTCTime, error) {
|
||||
return &value, err
|
||||
}
|
||||
|
||||
type ifreqEthtool struct {
|
||||
name [IFNAMSIZ]byte
|
||||
data unsafe.Pointer
|
||||
}
|
||||
|
||||
// IoctlGetEthtoolDrvinfo fetches ethtool driver information for the network
|
||||
// device specified by ifname.
|
||||
func IoctlGetEthtoolDrvinfo(fd int, ifname string) (*EthtoolDrvinfo, error) {
|
||||
// Leave room for terminating NULL byte.
|
||||
if len(ifname) >= IFNAMSIZ {
|
||||
return nil, EINVAL
|
||||
}
|
||||
|
||||
value := EthtoolDrvinfo{
|
||||
Cmd: ETHTOOL_GDRVINFO,
|
||||
}
|
||||
ifreq := ifreqEthtool{
|
||||
data: unsafe.Pointer(&value),
|
||||
}
|
||||
copy(ifreq.name[:], ifname)
|
||||
err := ioctl(fd, SIOCETHTOOL, uintptr(unsafe.Pointer(&ifreq)))
|
||||
runtime.KeepAlive(ifreq)
|
||||
return &value, err
|
||||
}
|
||||
|
||||
// IoctlGetWatchdogInfo fetches information about a watchdog device from the
|
||||
// Linux watchdog API. For more information, see:
|
||||
// https://www.kernel.org/doc/html/latest/watchdog/watchdog-api.html.
|
||||
@@ -857,16 +882,19 @@ type SockaddrVM struct {
|
||||
// CID and Port specify a context ID and port address for a VM socket.
|
||||
// Guests have a unique CID, and hosts may have a well-known CID of:
|
||||
// - VMADDR_CID_HYPERVISOR: refers to the hypervisor process.
|
||||
// - VMADDR_CID_LOCAL: refers to local communication (loopback).
|
||||
// - VMADDR_CID_HOST: refers to other processes on the host.
|
||||
CID uint32
|
||||
Port uint32
|
||||
raw RawSockaddrVM
|
||||
CID uint32
|
||||
Port uint32
|
||||
Flags uint8
|
||||
raw RawSockaddrVM
|
||||
}
|
||||
|
||||
func (sa *SockaddrVM) sockaddr() (unsafe.Pointer, _Socklen, error) {
|
||||
sa.raw.Family = AF_VSOCK
|
||||
sa.raw.Port = sa.Port
|
||||
sa.raw.Cid = sa.CID
|
||||
sa.raw.Flags = sa.Flags
|
||||
|
||||
return unsafe.Pointer(&sa.raw), SizeofSockaddrVM, nil
|
||||
}
|
||||
@@ -1171,8 +1199,9 @@ func anyToSockaddr(fd int, rsa *RawSockaddrAny) (Sockaddr, error) {
|
||||
case AF_VSOCK:
|
||||
pp := (*RawSockaddrVM)(unsafe.Pointer(rsa))
|
||||
sa := &SockaddrVM{
|
||||
CID: pp.Cid,
|
||||
Port: pp.Port,
|
||||
CID: pp.Cid,
|
||||
Port: pp.Port,
|
||||
Flags: pp.Flags,
|
||||
}
|
||||
return sa, nil
|
||||
case AF_BLUETOOTH:
|
||||
|
||||
+15
@@ -3698,6 +3698,21 @@ const (
|
||||
ETHTOOL_A_TUNNEL_INFO_MAX = 0x2
|
||||
)
|
||||
|
||||
type EthtoolDrvinfo struct {
|
||||
Cmd uint32
|
||||
Driver [32]byte
|
||||
Version [32]byte
|
||||
Fw_version [32]byte
|
||||
Bus_info [32]byte
|
||||
Erom_version [32]byte
|
||||
Reserved2 [12]byte
|
||||
N_priv_flags uint32
|
||||
N_stats uint32
|
||||
Testinfo_len uint32
|
||||
Eedump_len uint32
|
||||
Regdump_len uint32
|
||||
}
|
||||
|
||||
type (
|
||||
HIDRawReportDescriptor struct {
|
||||
Size uint32
|
||||
|
||||
Vendored
+2
-2
@@ -149,7 +149,7 @@ github.com/jesseduffield/go-git/v5/utils/merkletrie/filesystem
|
||||
github.com/jesseduffield/go-git/v5/utils/merkletrie/index
|
||||
github.com/jesseduffield/go-git/v5/utils/merkletrie/internal/frame
|
||||
github.com/jesseduffield/go-git/v5/utils/merkletrie/noder
|
||||
# github.com/jesseduffield/gocui v0.3.1-0.20210410011117-a2bb4baca390
|
||||
# github.com/jesseduffield/gocui v0.3.1-0.20210417110745-37f79434200d
|
||||
## explicit
|
||||
github.com/jesseduffield/gocui
|
||||
# github.com/jesseduffield/termbox-go v0.0.0-20200823212418-a2289ed6aafe
|
||||
@@ -235,7 +235,7 @@ golang.org/x/crypto/ssh/knownhosts
|
||||
golang.org/x/net/context
|
||||
golang.org/x/net/internal/socks
|
||||
golang.org/x/net/proxy
|
||||
# golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57
|
||||
# golang.org/x/sys v0.0.0-20210415045647-66c3f260301c
|
||||
## explicit
|
||||
golang.org/x/sys/cpu
|
||||
golang.org/x/sys/internal/unsafeheader
|
||||
|
||||
Reference in New Issue
Block a user