Compare commits

..
36 Commits
Author SHA1 Message Date
Jesse Duffield a99e6ba071 update release notes 2021-04-20 18:34:47 +10:00
Jesse Duffield 604ee02cd9 ignore east asian width setting to avoid broken frame rendering 2021-04-19 23:06:05 +10:00
Jesse Duffield 926a48a65b smarter sizing of command log panel 2021-04-19 18:09:01 +10:00
Jesse Duffield 98375dc902 refactor merge panel 2021-04-18 18:58:09 +10:00
Jesse Duffield e73de332a1 refactor line by line panel 2021-04-18 16:55:09 +10:00
Jesse Duffield b28b2d05bd force cursor to be at end of line when opening confirmation panel 2021-04-17 21:15:54 +10:00
Jesse Duffield 9e5f031553 bubble up tracked files in flat file view 2021-04-17 10:04:49 +10:00
Peijun Ma dab5ba363c Fix typo: scrool -> scroll 2021-04-14 19:11:30 +10:00
Jesse Duffield e42387d0da update keybindings 2021-04-12 23:40:20 +10:00
Jesse Duffield 730a03a3b2 fix race condition 2021-04-12 23:40:20 +10:00
Jesse Duffield 7d195b97c2 better squash description 2021-04-12 21:57:13 +10:00
Jesse Duffield 4fb2dba587 allow hiding random tip 2021-04-12 21:48:08 +10:00
Jesse Duffield 76697280c9 fix rendering issues caused by resizing 2021-04-12 21:48:08 +10:00
Jesse Duffield 0df6ac6140 bump gocui to fix resizing issue 2021-04-12 21:48:08 +10:00
Jesse Duffield 5453b71fd1 linting 2021-04-12 21:48:08 +10:00
Jesse Duffield 1f3070c882 fix up doc 2021-04-12 21:48:08 +10:00
Jesse Duffield 3b7e7a7f56 add random tip to command log 2021-04-12 21:48:08 +10:00
Jesse Duffield 06a8eb115c make command log size configurable 2021-04-11 23:36:34 +10:00
Jesse Duffield e4f0a470e9 print header for command log 2021-04-11 23:36:34 +10:00
Jesse Duffield adee0b8ccb add spans to i18n 2021-04-11 23:36:34 +10:00
Jesse Duffield 0bebfe454e pull out function 2021-04-11 23:36:34 +10:00
Jesse Duffield 84b0c3df4f ask question button 2021-04-11 22:07:29 +10:00
Jesse Duffield 764bd556f3 prettify config.md 2021-04-11 21:42:41 +10:00
Jesse Duffield 069c7c9d35 fix test 2021-04-11 17:07:49 +10:00
Jesse Duffield 393ce05860 allow focusing on command log view 2021-04-11 17:07:49 +10:00
Jesse Duffield cf78b86cb5 more support for command log and more code reuse for contexts 2021-04-11 17:07:49 +10:00
Jesse Duffield 4f03d7733a allow showing, hiding, and scrolling the extras panel 2021-04-11 17:07:49 +10:00
Jesse Duffield e3a14d546a support static boxes that go outside the available size 2021-04-11 17:07:49 +10:00
Jesse Duffield f2007f4d95 support scrolling extras view 2021-04-11 17:07:49 +10:00
Jesse Duffield 8969464b00 log TODO content when interactive rebasing 2021-04-11 17:07:49 +10:00
Jesse Duffield 6137d66914 no need to log this 2021-04-11 17:07:49 +10:00
Jesse Duffield 6fbe660f96 full coverage for logging commands 2021-04-11 17:07:49 +10:00
Jesse Duffield 74320f0075 more logging of commands 2021-04-11 17:07:49 +10:00
Jesse Duffield bfad972f0c fix bug where mixed reset is actually a soft reset 2021-04-11 17:07:49 +10:00
Jesse Duffield bb918b579a start adding support for logging of commands 2021-04-11 17:07:49 +10:00
Jesse Duffield e145090046 add cmdLog panel 2021-04-11 17:07:49 +10:00
82 changed files with 3275 additions and 2292 deletions
+241 -235
View File
@@ -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
+2 -2
View File
@@ -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
)
+12
View File
@@ -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=
+2 -1
View File
@@ -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))
}
}
+6 -2
View File
@@ -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)
}
-6
View File
@@ -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"
}
-1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 != "" {
-9
View File
@@ -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
View File
@@ -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)
}
+2 -5
View File
@@ -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)
}
+5 -8
View File
@@ -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
+1 -1
View File
@@ -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
+6 -6
View File
@@ -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) {
+10 -6
View File
@@ -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)
},
},
+2
View File
@@ -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()
+1 -1
View File
@@ -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
}
+14
View File
@@ -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",
+35
View File
@@ -0,0 +1,35 @@
package constants
type Docs struct {
CustomPagers string
CustomCommands string
CustomKeybindings string
Keybindings string
Undoing string
Config string
Tutorial string
}
var Links = struct {
Docs Docs
Issues string
Donate string
Discussions string
RepoUrl string
Releases string
}{
RepoUrl: "https://github.com/jesseduffield/lazygit",
Issues: "https://github.com/jesseduffield/lazygit/issues",
Donate: "https://github.com/sponsors/jesseduffield",
Discussions: "https://github.com/jesseduffield/lazygit/discussions",
Releases: "https://github.com/jesseduffield/lazygit/releases",
Docs: Docs{
CustomPagers: "https://github.com/jesseduffield/lazygit/blob/master/docs/Custom_Pagers.md",
CustomKeybindings: "https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md",
CustomCommands: "https://github.com/jesseduffield/lazygit/wiki/Custom-Commands-Compendium",
Keybindings: "https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings",
Undoing: "https://github.com/jesseduffield/lazygit/blob/master/docs/Undoing.md",
Config: "https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md",
Tutorial: "https://youtu.be/VDXvbHZYeKY",
},
}
+34 -2
View File
@@ -5,6 +5,8 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
const INFO_SECTION_PADDING = " "
func (gui *Gui) mainSectionChildren() []*boxlayout.Box {
currentWindow := gui.currentWindow()
@@ -130,6 +132,24 @@ func (gui *Gui) splitMainPanelSideBySide() bool {
}
}
func (gui *Gui) getExtrasWindowSize(screenHeight int) int {
if !gui.ShowExtrasWindow {
return 0
}
var baseSize int
if gui.currentStaticContext().GetKey() == COMMAND_LOG_CONTEXT_KEY {
baseSize = 1000 // my way of saying 'fill the available space'
} else if screenHeight < 40 {
baseSize = 1
} else {
baseSize = gui.Config.GetUserConfig().Gui.CommandLogSize
}
frameSize := 2
return baseSize + frameSize
}
func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions {
width, height := gui.g.Size()
@@ -146,6 +166,8 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
mainPanelsDirection = boxlayout.COLUMN
}
extrasWindowSize := gui.getExtrasWindowSize(height)
root := &boxlayout.Box{
Direction: boxlayout.ROW,
Children: []*boxlayout.Box{
@@ -159,9 +181,19 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map
ConditionalChildren: gui.sidePanelChildren,
},
{
Direction: mainPanelsDirection,
Direction: boxlayout.ROW,
Weight: mainSectionWeight,
Children: gui.mainSectionChildren(),
Children: []*boxlayout.Box{
{
Direction: mainPanelsDirection,
Children: gui.mainSectionChildren(),
Weight: 1,
},
{
Window: "extras",
Size: extrasWindowSize,
},
},
},
},
},
+77
View File
@@ -0,0 +1,77 @@
package gui
type BasicContext struct {
OnFocus func() error
OnFocusLost func() error
OnRender func() error
Kind ContextKind
Key ContextKey
ViewName string
WindowName string
OnGetOptionsMap func() map[string]string
ParentContext Context
// we can't know on the calling end whether a Context is actually a nil value without reflection, so we're storing this flag here to tell us. There has got to be a better way around this
hasParent bool
}
func (c *BasicContext) GetOptionsMap() map[string]string {
if c.OnGetOptionsMap != nil {
return c.OnGetOptionsMap()
}
return nil
}
func (c *BasicContext) SetParentContext(context Context) {
c.ParentContext = context
c.hasParent = true
}
func (c *BasicContext) GetParentContext() (Context, bool) {
return c.ParentContext, c.hasParent
}
func (c *BasicContext) SetWindowName(windowName string) {
c.WindowName = windowName
}
func (c *BasicContext) GetWindowName() string {
windowName := c.WindowName
if windowName != "" {
return windowName
}
// TODO: actually set this for everything so we don't default to the view name
return c.ViewName
}
func (c *BasicContext) HandleRender() error {
if c.OnRender != nil {
return c.OnRender()
}
return nil
}
func (c *BasicContext) GetViewName() string {
return c.ViewName
}
func (c *BasicContext) HandleFocus() error {
return c.OnFocus()
}
func (c *BasicContext) HandleFocusLost() error {
if c.OnFocusLost != nil {
return c.OnFocusLost()
}
return nil
}
func (c *BasicContext) GetKind() ContextKind {
return c.Kind
}
func (c *BasicContext) GetKey() ContextKey {
return c.Key
}
+5
View File
@@ -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
+21
View File
@@ -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
View File
@@ -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
}
+1 -1
View File
@@ -148,7 +148,7 @@ func (gui *Gui) HandlePasteCommits() error {
prompt: gui.Tr.SureCherryPick,
handleConfirm: func() error {
return gui.WithWaitingStatus(gui.Tr.CherryPickingStatus, func() error {
err := gui.GitCommand.CherryPickCommits(gui.State.Modes.CherryPicking.CherryPickedCommits)
err := gui.GitCommand.WithSpan(gui.Tr.Spans.CherryPick).CherryPickCommits(gui.State.Modes.CherryPicking.CherryPickedCommits)
return gui.handleGenericMergeCommandResult(err)
})
},
+185
View File
@@ -0,0 +1,185 @@
package gui
import (
"fmt"
"math/rand"
"strings"
"time"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/constants"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) GetOnRunCommand() func(entry oscommands.CmdLogEntry) {
// closing over this so that nobody else can modify it
currentSpan := ""
return func(entry oscommands.CmdLogEntry) {
if gui.Views.Extras == nil {
return
}
gui.Views.Extras.Autoscroll = true
if entry.GetSpan() != currentSpan {
fmt.Fprint(gui.Views.Extras, "\n"+utils.ColoredString(entry.GetSpan(), color.FgYellow))
currentSpan = entry.GetSpan()
}
clrAttr := theme.DefaultTextColor
if !entry.GetCommandLine() {
clrAttr = color.FgMagenta
}
gui.CmdLog = append(gui.CmdLog, entry.GetCmdStr())
indentedCmdStr := " " + strings.Replace(entry.GetCmdStr(), "\n", "\n ", -1)
fmt.Fprint(gui.Views.Extras, "\n"+utils.ColoredString(indentedCmdStr, clrAttr))
}
}
func (gui *Gui) printCommandLogHeader() {
introStr := fmt.Sprintf(
gui.Tr.CommandLogHeader,
gui.getKeyDisplay(gui.Config.GetUserConfig().Keybinding.Universal.ExtrasMenu),
)
fmt.Fprintln(gui.Views.Extras, utils.ColoredString(introStr, color.FgCyan))
if gui.Config.GetUserConfig().Gui.ShowRandomTip {
fmt.Fprintf(
gui.Views.Extras,
"%s: %s",
utils.ColoredString(gui.Tr.RandomTip, color.FgYellow),
utils.ColoredString(gui.getRandomTip(), color.FgGreen),
)
}
}
func (gui *Gui) getRandomTip() string {
config := gui.Config.GetUserConfig().Keybinding
formattedKey := func(key string) string {
return gui.getKeyDisplay(key)
}
tips := []string{
// keybindings and lazygit-specific advice
fmt.Sprintf(
"To force push, press '%s' and then if the push is rejected you will be asked if you want to force push",
formattedKey(config.Universal.PushFiles),
),
fmt.Sprintf(
"To filter commits by path, press '%s'",
formattedKey(config.Universal.FilteringMenu),
),
fmt.Sprintf(
"To start an interactive rebase, press '%s' on a commit. You can always abort the rebase by pressing '%s' and selecting 'abort'",
formattedKey(config.Universal.Edit),
formattedKey(config.Universal.CreateRebaseOptionsMenu),
),
fmt.Sprintf(
"In flat file view, merge conflicts are sorted to the top. To switch to flat file view press '%s'",
formattedKey(config.Files.ToggleTreeView),
),
"If you want to learn Go and can think of ways to improve lazygit, join the team! Click 'Ask Question' and express your interest",
fmt.Sprintf(
"If you press '%s'/'%s' you can undo/redo your changes. Be wary though, this only applies to branches/commits, so only do this if your worktree is clear.\nDocs: %s",
formattedKey(config.Universal.Undo),
formattedKey(config.Universal.Redo),
constants.Links.Docs.Undoing,
),
fmt.Sprintf(
"to hard reset onto your current upstream branch, press '%s' in the files panel",
formattedKey(config.Files.ViewResetOptions),
),
fmt.Sprintf(
"To push a tag, navigate to the tag in the tags tab and press '%s'",
formattedKey(config.Branches.PushTag),
),
fmt.Sprintf(
"You can view the individual files of a stash entry by pressing '%s'",
formattedKey(config.Universal.GoInto),
),
fmt.Sprintf(
"You can diff two commits by pressing '%s' one one commit and then navigating to the other. You can then press '%s' to view the files of the diff",
formattedKey(config.Universal.DiffingMenu),
formattedKey(config.Universal.GoInto),
),
fmt.Sprintf(
"press '%s' on a commit to drop it (delete it)",
formattedKey(config.Universal.Remove),
),
fmt.Sprintf(
"If you need to pull out the big guns to resolve merge conflicts, you can press '%s' in the files panel to open 'git mergetool'",
formattedKey(config.Files.OpenMergeTool),
),
fmt.Sprintf(
"To revert a commit, press '%s' on that commit",
formattedKey(config.Commits.RevertCommit),
),
fmt.Sprintf(
"To escape a mode, for example cherry-picking, patch-building, diffing, or filtering mode, you can just spam the '%s' button. Unless of course you have `quitOnTopLevelReturn` enabled in your config",
formattedKey(config.Universal.Return),
),
fmt.Sprintf(
"To search for a string in your panel, press '%s'",
formattedKey(config.Universal.StartSearch),
),
fmt.Sprintf(
"You can page through the items of a panel using '%s' and '%s'",
formattedKey(config.Universal.PrevPage),
formattedKey(config.Universal.NextPage),
),
fmt.Sprintf(
"You can jump to the top/bottom of a panel using '%s' and '%s'",
formattedKey(config.Universal.GotoTop),
formattedKey(config.Universal.GotoBottom),
),
fmt.Sprintf(
"To collapse/expand a directory, press '%s'",
formattedKey(config.Universal.GoInto),
),
fmt.Sprintf(
"You can append your staged changes to an older commit by pressing '%s' on that commit",
formattedKey(config.Commits.AmendToCommit),
),
fmt.Sprintf(
"You can amend the last commit with your new file changes by pressing '%s' in the files panel",
formattedKey(config.Files.AmendLastCommit),
),
fmt.Sprintf(
"You can now navigate the side panels with '%s' and '%s'",
formattedKey(config.Universal.NextBlockAlt2),
formattedKey(config.Universal.PrevBlockAlt2),
),
"You can use lazygit with a bare repo by passing the --git-dir and --work-tree arguments as you would for the git CLI",
// general advice
"`git commit` is really just the programmer equivalent of saving your game. Always do it before embarking on an ambitious change!",
"Try to separate commits that refactor code from commits that add new functionality: if they're squashed into the one commit, it can be hard to spot what's new.",
"If you ever want to experiment, it's easy to create a new branch off your current one and go nuts, then delete it afterwards",
"Always read through the diff of your changes before assigning somebody to review your code. Better for you to catch any silly mistakes than your colleagues!",
"If something goes wrong, you can always checkout a commit from your reflog to return to an earlier state",
"The stash is a good place to save snippets of code that you always find yourself adding when debugging.",
// links
fmt.Sprintf(
"If you want a git diff with syntax colouring, check out lazygit's integration with delta:\n%s",
constants.Links.Docs.CustomPagers,
),
fmt.Sprintf(
"You can build your own custom menus and commands to run from within lazygit. For examples see:\n%s",
constants.Links.Docs.CustomCommands,
),
fmt.Sprintf(
"If you ever find a bug, do not hesistate to raise an issue on the repo:\n%s",
constants.Links.Issues,
),
}
rand.Seed(time.Now().UnixNano())
randomIndex := rand.Intn(len(tips))
return tips[randomIndex]
}
+2 -2
View File
@@ -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
}
+7 -4
View File
@@ -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
View File
@@ -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)
}
+9 -12
View File
@@ -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
View File
@@ -13,111 +13,9 @@ const (
MAIN_CONTEXT
TEMPORARY_POPUP
PERSISTENT_POPUP
EXTRAS_CONTEXT
)
type ContextKey string
const (
STATUS_CONTEXT_KEY ContextKey = "status"
FILES_CONTEXT_KEY ContextKey = "files"
LOCAL_BRANCHES_CONTEXT_KEY ContextKey = "localBranches"
REMOTES_CONTEXT_KEY ContextKey = "remotes"
REMOTE_BRANCHES_CONTEXT_KEY ContextKey = "remoteBranches"
TAGS_CONTEXT_KEY ContextKey = "tags"
BRANCH_COMMITS_CONTEXT_KEY ContextKey = "commits"
REFLOG_COMMITS_CONTEXT_KEY ContextKey = "reflogCommits"
SUB_COMMITS_CONTEXT_KEY ContextKey = "subCommits"
COMMIT_FILES_CONTEXT_KEY ContextKey = "commitFiles"
STASH_CONTEXT_KEY ContextKey = "stash"
MAIN_NORMAL_CONTEXT_KEY ContextKey = "normal"
MAIN_MERGING_CONTEXT_KEY ContextKey = "merging"
MAIN_PATCH_BUILDING_CONTEXT_KEY ContextKey = "patchBuilding"
MAIN_STAGING_CONTEXT_KEY ContextKey = "staging"
MENU_CONTEXT_KEY ContextKey = "menu"
CREDENTIALS_CONTEXT_KEY ContextKey = "credentials"
CONFIRMATION_CONTEXT_KEY ContextKey = "confirmation"
SEARCH_CONTEXT_KEY ContextKey = "search"
COMMIT_MESSAGE_CONTEXT_KEY ContextKey = "commitMessage"
SUBMODULES_CONTEXT_KEY ContextKey = "submodules"
SUGGESTIONS_CONTEXT_KEY ContextKey = "suggestions"
)
var allContextKeys = []ContextKey{
STATUS_CONTEXT_KEY,
FILES_CONTEXT_KEY,
LOCAL_BRANCHES_CONTEXT_KEY,
REMOTES_CONTEXT_KEY,
REMOTE_BRANCHES_CONTEXT_KEY,
TAGS_CONTEXT_KEY,
BRANCH_COMMITS_CONTEXT_KEY,
REFLOG_COMMITS_CONTEXT_KEY,
SUB_COMMITS_CONTEXT_KEY,
COMMIT_FILES_CONTEXT_KEY,
STASH_CONTEXT_KEY,
MAIN_NORMAL_CONTEXT_KEY,
MAIN_MERGING_CONTEXT_KEY,
MAIN_PATCH_BUILDING_CONTEXT_KEY,
MAIN_STAGING_CONTEXT_KEY,
MENU_CONTEXT_KEY,
CREDENTIALS_CONTEXT_KEY,
CONFIRMATION_CONTEXT_KEY,
SEARCH_CONTEXT_KEY,
COMMIT_MESSAGE_CONTEXT_KEY,
SUBMODULES_CONTEXT_KEY,
SUGGESTIONS_CONTEXT_KEY,
}
type ContextTree struct {
Status Context
Files *ListContext
Submodules *ListContext
Menu *ListContext
Branches *ListContext
Remotes *ListContext
RemoteBranches *ListContext
Tags *ListContext
BranchCommits *ListContext
CommitFiles *ListContext
ReflogCommits *ListContext
SubCommits *ListContext
Stash *ListContext
Suggestions *ListContext
Normal Context
Staging Context
PatchBuilding Context
Merging Context
Credentials Context
Confirmation Context
CommitMessage Context
Search Context
}
func (gui *Gui) allContexts() []Context {
return []Context{
gui.State.Contexts.Status,
gui.State.Contexts.Files,
gui.State.Contexts.Submodules,
gui.State.Contexts.Branches,
gui.State.Contexts.Remotes,
gui.State.Contexts.RemoteBranches,
gui.State.Contexts.Tags,
gui.State.Contexts.BranchCommits,
gui.State.Contexts.CommitFiles,
gui.State.Contexts.ReflogCommits,
gui.State.Contexts.Stash,
gui.State.Contexts.Menu,
gui.State.Contexts.Confirmation,
gui.State.Contexts.Credentials,
gui.State.Contexts.CommitMessage,
gui.State.Contexts.Normal,
gui.State.Contexts.Staging,
gui.State.Contexts.Merging,
gui.State.Contexts.PatchBuilding,
gui.State.Contexts.SubCommits,
gui.State.Contexts.Suggestions,
}
}
type Context interface {
HandleFocus() error
HandleFocusLost() error
@@ -134,170 +32,6 @@ type Context interface {
GetOptionsMap() map[string]string
}
type BasicContext struct {
OnFocus func() error
OnFocusLost func() error
OnRender func() error
OnGetOptionsMap func() map[string]string
Kind ContextKind
Key ContextKey
ViewName string
}
func (c BasicContext) GetOptionsMap() map[string]string {
if c.OnGetOptionsMap != nil {
return c.OnGetOptionsMap()
}
return nil
}
func (c BasicContext) SetWindowName(windowName string) {
panic("can't set window name on basic context")
}
func (c BasicContext) GetWindowName() string {
// TODO: fix this up
return c.GetViewName()
}
func (c BasicContext) SetParentContext(Context) {
panic("can't set parent context on basic context")
}
func (c BasicContext) GetParentContext() (Context, bool) {
return nil, false
}
func (c BasicContext) HandleRender() error {
if c.OnRender != nil {
return c.OnRender()
}
return nil
}
func (c BasicContext) GetViewName() string {
return c.ViewName
}
func (c BasicContext) HandleFocus() error {
return c.OnFocus()
}
func (c BasicContext) HandleFocusLost() error {
if c.OnFocusLost != nil {
return c.OnFocusLost()
}
return nil
}
func (c BasicContext) GetKind() ContextKind {
return c.Kind
}
func (c BasicContext) GetKey() ContextKey {
return c.Key
}
func (gui *Gui) contextTree() ContextTree {
return ContextTree{
Status: BasicContext{
OnFocus: gui.handleStatusSelect,
Kind: SIDE_CONTEXT,
ViewName: "status",
Key: STATUS_CONTEXT_KEY,
},
Files: gui.filesListContext(),
Submodules: gui.submodulesListContext(),
Menu: gui.menuListContext(),
Remotes: gui.remotesListContext(),
RemoteBranches: gui.remoteBranchesListContext(),
BranchCommits: gui.branchCommitsListContext(),
CommitFiles: gui.commitFilesListContext(),
ReflogCommits: gui.reflogCommitsListContext(),
SubCommits: gui.subCommitsListContext(),
Branches: gui.branchesListContext(),
Tags: gui.tagsListContext(),
Stash: gui.stashListContext(),
Normal: BasicContext{
OnFocus: func() error {
return nil // TODO: should we do something here? We should allow for scrolling the panel
},
Kind: MAIN_CONTEXT,
ViewName: "main",
Key: MAIN_NORMAL_CONTEXT_KEY,
},
Staging: BasicContext{
OnFocus: func() error {
return nil
// TODO: centralise the code here
// return gui.refreshStagingPanel(false, -1)
},
Kind: MAIN_CONTEXT,
ViewName: "main",
Key: MAIN_STAGING_CONTEXT_KEY,
},
PatchBuilding: BasicContext{
OnFocus: func() error {
return nil
// TODO: centralise the code here
// return gui.refreshPatchBuildingPanel(-1)
},
Kind: MAIN_CONTEXT,
ViewName: "main",
Key: MAIN_PATCH_BUILDING_CONTEXT_KEY,
},
Merging: BasicContext{
OnFocus: gui.refreshMergePanelWithLock,
Kind: MAIN_CONTEXT,
ViewName: "main",
Key: MAIN_MERGING_CONTEXT_KEY,
OnGetOptionsMap: gui.getMergingOptions,
},
Credentials: BasicContext{
OnFocus: gui.handleCredentialsViewFocused,
Kind: PERSISTENT_POPUP,
ViewName: "credentials",
Key: CREDENTIALS_CONTEXT_KEY,
},
Confirmation: BasicContext{
OnFocus: func() error { return nil },
Kind: TEMPORARY_POPUP,
ViewName: "confirmation",
Key: CONFIRMATION_CONTEXT_KEY,
},
Suggestions: gui.suggestionsListContext(),
CommitMessage: BasicContext{
OnFocus: gui.handleCommitMessageFocused,
Kind: PERSISTENT_POPUP,
ViewName: "commitMessage",
Key: COMMIT_MESSAGE_CONTEXT_KEY,
},
Search: BasicContext{
OnFocus: func() error { return nil },
Kind: PERSISTENT_POPUP,
ViewName: "search",
Key: SEARCH_CONTEXT_KEY,
},
}
}
func (tree ContextTree) initialViewContextMap() map[string]Context {
return map[string]Context{
"status": tree.Status,
"files": tree.Files,
"branches": tree.Branches,
"commits": tree.BranchCommits,
"commitFiles": tree.CommitFiles,
"stash": tree.Stash,
"menu": tree.Menu,
"confirmation": tree.Confirmation,
"credentials": tree.Credentials,
"commitMessage": tree.CommitMessage,
"main": tree.Normal,
"secondary": tree.Normal,
}
}
func (gui *Gui) popupViewNames() []string {
result := []string{}
for _, context := range gui.allContexts() {
@@ -309,52 +43,6 @@ func (gui *Gui) popupViewNames() []string {
return result
}
func (tree ContextTree) initialViewTabContextMap() map[string][]tabContext {
return map[string][]tabContext{
"branches": {
{
tab: "Local Branches",
contexts: []Context{tree.Branches},
},
{
tab: "Remotes",
contexts: []Context{
tree.Remotes,
tree.RemoteBranches,
},
},
{
tab: "Tags",
contexts: []Context{tree.Tags},
},
},
"commits": {
{
tab: "Commits",
contexts: []Context{tree.BranchCommits},
},
{
tab: "Reflog",
contexts: []Context{
tree.ReflogCommits,
},
},
},
"files": {
{
tab: "Files",
contexts: []Context{tree.Files},
},
{
tab: "Submodules",
contexts: []Context{
tree.Submodules,
},
},
},
}
}
func (gui *Gui) currentContextKeyIgnoringPopups() ContextKey {
gui.State.ContextManager.RLock()
defer gui.State.ContextManager.RUnlock()
@@ -416,7 +104,7 @@ func (gui *Gui) pushContextDirect(c Context) error {
}
}
gui.State.ContextManager.ContextStack = []Context{c}
} else if len(gui.State.ContextManager.ContextStack) == 0 || gui.State.ContextManager.ContextStack[len(gui.State.ContextManager.ContextStack)-1].GetKey() != c.GetKey() {
} else if len(gui.State.ContextManager.ContextStack) == 0 || gui.currentContextWithoutLock().GetKey() != c.GetKey() {
// Do not append if the one at the end is the same context (e.g. opening a menu from a menu)
// In that case we'll just close the menu entirely when the user hits escape.
@@ -580,6 +268,10 @@ func (gui *Gui) currentContext() Context {
gui.State.ContextManager.RLock()
defer gui.State.ContextManager.RUnlock()
return gui.currentContextWithoutLock()
}
func (gui *Gui) currentContextWithoutLock() Context {
if len(gui.State.ContextManager.ContextStack) == 0 {
return gui.defaultSideContext()
}
@@ -621,6 +313,29 @@ func (gui *Gui) currentSideContext() Context {
return gui.defaultSideContext()
}
// static as opposed to popup
func (gui *Gui) currentStaticContext() Context {
gui.State.ContextManager.RLock()
defer gui.State.ContextManager.RUnlock()
stack := gui.State.ContextManager.ContextStack
if len(stack) == 0 {
return gui.defaultSideContext()
}
// find the first context in the stack without a popup type
for i := range stack {
context := stack[len(stack)-1-i]
if context.GetKind() != TEMPORARY_POPUP && context.GetKind() != PERSISTENT_POPUP {
return context
}
}
return gui.defaultSideContext()
}
func (gui *Gui) defaultSideContext() Context {
if gui.State.Modes.Filtering.Active() {
return gui.State.Contexts.BranchCommits
@@ -670,7 +385,7 @@ func (gui *Gui) onViewFocusChange() error {
currentView := gui.g.CurrentView()
for _, view := range gui.g.Views() {
view.Highlight = view.Name() != "main" && view == currentView
view.Highlight = view.Name() != "main" && view.Name() != "extras" && view == currentView
}
return nil
}
+266
View File
@@ -0,0 +1,266 @@
package gui
type ContextKey string
const (
STATUS_CONTEXT_KEY ContextKey = "status"
FILES_CONTEXT_KEY ContextKey = "files"
LOCAL_BRANCHES_CONTEXT_KEY ContextKey = "localBranches"
REMOTES_CONTEXT_KEY ContextKey = "remotes"
REMOTE_BRANCHES_CONTEXT_KEY ContextKey = "remoteBranches"
TAGS_CONTEXT_KEY ContextKey = "tags"
BRANCH_COMMITS_CONTEXT_KEY ContextKey = "commits"
REFLOG_COMMITS_CONTEXT_KEY ContextKey = "reflogCommits"
SUB_COMMITS_CONTEXT_KEY ContextKey = "subCommits"
COMMIT_FILES_CONTEXT_KEY ContextKey = "commitFiles"
STASH_CONTEXT_KEY ContextKey = "stash"
MAIN_NORMAL_CONTEXT_KEY ContextKey = "normal"
MAIN_MERGING_CONTEXT_KEY ContextKey = "merging"
MAIN_PATCH_BUILDING_CONTEXT_KEY ContextKey = "patchBuilding"
MAIN_STAGING_CONTEXT_KEY ContextKey = "staging"
MENU_CONTEXT_KEY ContextKey = "menu"
CREDENTIALS_CONTEXT_KEY ContextKey = "credentials"
CONFIRMATION_CONTEXT_KEY ContextKey = "confirmation"
SEARCH_CONTEXT_KEY ContextKey = "search"
COMMIT_MESSAGE_CONTEXT_KEY ContextKey = "commitMessage"
SUBMODULES_CONTEXT_KEY ContextKey = "submodules"
SUGGESTIONS_CONTEXT_KEY ContextKey = "suggestions"
COMMAND_LOG_CONTEXT_KEY ContextKey = "cmdLog"
)
var allContextKeys = []ContextKey{
STATUS_CONTEXT_KEY,
FILES_CONTEXT_KEY,
LOCAL_BRANCHES_CONTEXT_KEY,
REMOTES_CONTEXT_KEY,
REMOTE_BRANCHES_CONTEXT_KEY,
TAGS_CONTEXT_KEY,
BRANCH_COMMITS_CONTEXT_KEY,
REFLOG_COMMITS_CONTEXT_KEY,
SUB_COMMITS_CONTEXT_KEY,
COMMIT_FILES_CONTEXT_KEY,
STASH_CONTEXT_KEY,
MAIN_NORMAL_CONTEXT_KEY,
MAIN_MERGING_CONTEXT_KEY,
MAIN_PATCH_BUILDING_CONTEXT_KEY,
MAIN_STAGING_CONTEXT_KEY,
MENU_CONTEXT_KEY,
CREDENTIALS_CONTEXT_KEY,
CONFIRMATION_CONTEXT_KEY,
SEARCH_CONTEXT_KEY,
COMMIT_MESSAGE_CONTEXT_KEY,
SUBMODULES_CONTEXT_KEY,
SUGGESTIONS_CONTEXT_KEY,
COMMAND_LOG_CONTEXT_KEY,
}
type ContextTree struct {
Status Context
Files *ListContext
Submodules *ListContext
Menu *ListContext
Branches *ListContext
Remotes *ListContext
RemoteBranches *ListContext
Tags *ListContext
BranchCommits *ListContext
CommitFiles *ListContext
ReflogCommits *ListContext
SubCommits *ListContext
Stash *ListContext
Suggestions *ListContext
Normal Context
Staging Context
PatchBuilding Context
Merging Context
Credentials Context
Confirmation Context
CommitMessage Context
Search Context
CommandLog Context
}
func (gui *Gui) allContexts() []Context {
return []Context{
gui.State.Contexts.Status,
gui.State.Contexts.Files,
gui.State.Contexts.Submodules,
gui.State.Contexts.Branches,
gui.State.Contexts.Remotes,
gui.State.Contexts.RemoteBranches,
gui.State.Contexts.Tags,
gui.State.Contexts.BranchCommits,
gui.State.Contexts.CommitFiles,
gui.State.Contexts.ReflogCommits,
gui.State.Contexts.Stash,
gui.State.Contexts.Menu,
gui.State.Contexts.Confirmation,
gui.State.Contexts.Credentials,
gui.State.Contexts.CommitMessage,
gui.State.Contexts.Normal,
gui.State.Contexts.Staging,
gui.State.Contexts.Merging,
gui.State.Contexts.PatchBuilding,
gui.State.Contexts.SubCommits,
gui.State.Contexts.Suggestions,
gui.State.Contexts.CommandLog,
}
}
func (gui *Gui) contextTree() ContextTree {
return ContextTree{
Status: &BasicContext{
OnFocus: gui.handleStatusSelect,
Kind: SIDE_CONTEXT,
ViewName: "status",
Key: STATUS_CONTEXT_KEY,
},
Files: gui.filesListContext(),
Submodules: gui.submodulesListContext(),
Menu: gui.menuListContext(),
Remotes: gui.remotesListContext(),
RemoteBranches: gui.remoteBranchesListContext(),
BranchCommits: gui.branchCommitsListContext(),
CommitFiles: gui.commitFilesListContext(),
ReflogCommits: gui.reflogCommitsListContext(),
SubCommits: gui.subCommitsListContext(),
Branches: gui.branchesListContext(),
Tags: gui.tagsListContext(),
Stash: gui.stashListContext(),
Normal: &BasicContext{
OnFocus: func() error {
return nil // TODO: should we do something here? We should allow for scrolling the panel
},
Kind: MAIN_CONTEXT,
ViewName: "main",
Key: MAIN_NORMAL_CONTEXT_KEY,
},
Staging: &BasicContext{
OnFocus: func() error {
return nil
// TODO: centralise the code here
// return gui.refreshStagingPanel(false, -1)
},
Kind: MAIN_CONTEXT,
ViewName: "main",
Key: MAIN_STAGING_CONTEXT_KEY,
},
PatchBuilding: &BasicContext{
OnFocus: func() error {
return nil
// TODO: centralise the code here
// return gui.refreshPatchBuildingPanel(-1)
},
Kind: MAIN_CONTEXT,
ViewName: "main",
Key: MAIN_PATCH_BUILDING_CONTEXT_KEY,
},
Merging: &BasicContext{
OnFocus: gui.refreshMergePanelWithLock,
Kind: MAIN_CONTEXT,
ViewName: "main",
Key: MAIN_MERGING_CONTEXT_KEY,
OnGetOptionsMap: gui.getMergingOptions,
},
Credentials: &BasicContext{
OnFocus: gui.handleCredentialsViewFocused,
Kind: PERSISTENT_POPUP,
ViewName: "credentials",
Key: CREDENTIALS_CONTEXT_KEY,
},
Confirmation: &BasicContext{
OnFocus: func() error { return nil },
Kind: TEMPORARY_POPUP,
ViewName: "confirmation",
Key: CONFIRMATION_CONTEXT_KEY,
},
Suggestions: gui.suggestionsListContext(),
CommitMessage: &BasicContext{
OnFocus: gui.handleCommitMessageFocused,
Kind: PERSISTENT_POPUP,
ViewName: "commitMessage",
Key: COMMIT_MESSAGE_CONTEXT_KEY,
},
Search: &BasicContext{
OnFocus: func() error { return nil },
Kind: PERSISTENT_POPUP,
ViewName: "search",
Key: SEARCH_CONTEXT_KEY,
},
CommandLog: &BasicContext{
OnFocus: func() error { return nil },
Kind: EXTRAS_CONTEXT,
ViewName: "extras",
Key: COMMAND_LOG_CONTEXT_KEY,
OnGetOptionsMap: gui.getMergingOptions,
OnFocusLost: func() error {
gui.Views.Extras.Autoscroll = true
return nil
},
},
}
}
func (tree ContextTree) initialViewContextMap() map[string]Context {
return map[string]Context{
"status": tree.Status,
"files": tree.Files,
"branches": tree.Branches,
"commits": tree.BranchCommits,
"commitFiles": tree.CommitFiles,
"stash": tree.Stash,
"menu": tree.Menu,
"confirmation": tree.Confirmation,
"credentials": tree.Credentials,
"commitMessage": tree.CommitMessage,
"main": tree.Normal,
"secondary": tree.Normal,
"extras": tree.CommandLog,
}
}
func (tree ContextTree) initialViewTabContextMap() map[string][]tabContext {
return map[string][]tabContext{
"branches": {
{
tab: "Local Branches",
contexts: []Context{tree.Branches},
},
{
tab: "Remotes",
contexts: []Context{
tree.Remotes,
tree.RemoteBranches,
},
},
{
tab: "Tags",
contexts: []Context{tree.Tags},
},
},
"commits": {
{
tab: "Commits",
contexts: []Context{tree.BranchCommits},
},
{
tab: "Reflog",
contexts: []Context{
tree.ReflogCommits,
},
},
},
"files": {
{
tab: "Files",
contexts: []Context{tree.Files},
},
{
tab: "Submodules",
contexts: []Context{
tree.Submodules,
},
},
},
}
}
+1 -1
View File
@@ -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{})
+4 -4
View File
@@ -12,7 +12,7 @@ func (gui *Gui) handleCreateDiscardMenu() error {
{
displayString: gui.Tr.LcDiscardAllChanges,
onPress: func() error {
if err := gui.GitCommand.DiscardAllDirChanges(node); err != nil {
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.DiscardAllChangesInDirectory).DiscardAllDirChanges(node); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}})
@@ -24,7 +24,7 @@ func (gui *Gui) handleCreateDiscardMenu() error {
menuItems = append(menuItems, &menuItem{
displayString: gui.Tr.LcDiscardUnstagedChanges,
onPress: func() error {
if err := gui.GitCommand.DiscardUnstagedDirChanges(node); err != nil {
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.DiscardUnstagedChangesInDirectory).DiscardUnstagedDirChanges(node); err != nil {
return gui.surfaceError(err)
}
@@ -52,7 +52,7 @@ func (gui *Gui) handleCreateDiscardMenu() error {
{
displayString: gui.Tr.LcDiscardAllChanges,
onPress: func() error {
if err := gui.GitCommand.DiscardAllFileChanges(file); err != nil {
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.DiscardAllChangesInFile).DiscardAllFileChanges(file); err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}})
@@ -64,7 +64,7 @@ func (gui *Gui) handleCreateDiscardMenu() error {
menuItems = append(menuItems, &menuItem{
displayString: gui.Tr.LcDiscardUnstagedChanges,
onPress: func() error {
if err := gui.GitCommand.DiscardUnstagedFileChanges(file); err != nil {
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.DiscardAllUnstagedChangesInFile).DiscardUnstagedFileChanges(file); err != nil {
return gui.surfaceError(err)
}
+49
View File
@@ -0,0 +1,49 @@
package gui
func (gui *Gui) handleCreateExtrasMenuPanel() error {
menuItems := []*menuItem{
{
displayString: gui.Tr.ToggleShowCommandLog,
onPress: func() error {
currentContext := gui.currentStaticContext()
if gui.ShowExtrasWindow && currentContext.GetKey() == COMMAND_LOG_CONTEXT_KEY {
if err := gui.returnFromContext(); err != nil {
return err
}
}
gui.ShowExtrasWindow = !gui.ShowExtrasWindow
return nil
},
},
{
displayString: gui.Tr.FocusCommandLog,
onPress: func() error {
return gui.handleFocusCommandLog()
},
},
}
return gui.createMenu(gui.Tr.CommandLog, menuItems, createMenuOptions{showCancel: true})
}
func (gui *Gui) handleFocusCommandLog() error {
gui.ShowExtrasWindow = true
gui.State.Contexts.CommandLog.SetParentContext(gui.currentSideContext())
return gui.pushContext(gui.State.Contexts.CommandLog)
}
func (gui *Gui) scrollUpExtra() error {
gui.Views.Extras.Autoscroll = false
return gui.scrollUpView(gui.Views.Extras)
}
func (gui *Gui) scrollDownExtra() error {
gui.Views.Extras.Autoscroll = false
if err := gui.scrollDownView(gui.Views.Extras); err != nil {
return err
}
return nil
}
+41 -30
View File
@@ -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)
},
},
}
+28 -3
View File
@@ -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}
+36 -10
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
})
}
+52
View File
@@ -0,0 +1,52 @@
package gui
import (
"fmt"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/constants"
)
func (gui *Gui) informationStr() string {
for _, mode := range gui.modeStatuses() {
if mode.isActive() {
return mode.description()
}
}
if gui.g.Mouse {
donate := color.New(color.FgMagenta, color.Underline).Sprint(gui.Tr.Donate)
askQuestion := color.New(color.FgYellow, color.Underline).Sprint(gui.Tr.AskQuestion)
return fmt.Sprintf("%s %s %s", donate, askQuestion, gui.Config.GetVersion())
} else {
return gui.Config.GetVersion()
}
}
func (gui *Gui) handleInfoClick() error {
if !gui.g.Mouse {
return nil
}
view := gui.Views.Information
cx, _ := view.Cursor()
width, _ := view.Size()
for _, mode := range gui.modeStatuses() {
if mode.isActive() {
if width-cx > len(gui.Tr.ResetInParentheses) {
return nil
}
return mode.reset()
}
}
// if we're not in an active mode we show the donate button
if cx <= len(gui.Tr.Donate) {
return gui.OSCommand.OpenLink(constants.Links.Donate)
} else if cx <= len(gui.Tr.Donate)+1+len(gui.Tr.AskQuestion) {
return gui.OSCommand.OpenLink(constants.Links.Discussions)
}
return nil
}
+70 -4
View File
@@ -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
View File
@@ -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,
+191
View File
@@ -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
View File
@@ -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
View File
@@ -1,21 +1,11 @@
package gui
import (
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type ListContext struct {
ViewName string
ContextKey ContextKey
GetItemsLength func() int
GetDisplayStrings func() [][]string
OnFocus func() error
OnFocusLost func() error
OnClickSelectedItem func() error
OnGetOptionsMap func() map[string]string
// the boolean here tells us whether the item is nil. This is needed because you can't work it out on the calling end once the pointer is wrapped in an interface (unless you want to use reflection)
SelectedItem func() (ListItem, bool)
@@ -23,11 +13,8 @@ type ListContext struct {
Gui *Gui
ResetMainViewOriginOnFocus bool
Kind ContextKind
ParentContext Context
// we can't know on the calling end whether a Context is actually a nil value without reflection, so we're storing this flag here to tell us. There has got to be a better way around this.
hasParent bool
WindowName string
*BasicContext
}
type IListPanelState interface {
@@ -47,30 +34,6 @@ func (lc *ListContext) GetSelectedItem() (ListItem, bool) {
return lc.SelectedItem()
}
func (lc *ListContext) SetWindowName(windowName string) {
lc.WindowName = windowName
}
func (lc *ListContext) GetWindowName() string {
windowName := lc.WindowName
if windowName != "" {
return windowName
}
// TODO: actually set this for everything so we don't default to the view name
return lc.ViewName
}
func (lc *ListContext) SetParentContext(c Context) {
lc.ParentContext = c
lc.hasParent = true
}
func (lc *ListContext) GetParentContext() (Context, bool) {
return lc.ParentContext, lc.hasParent
}
func (lc *ListContext) GetSelectedItemId() string {
item, ok := lc.SelectedItem()
@@ -81,13 +44,6 @@ func (lc *ListContext) GetSelectedItemId() string {
return item.ID()
}
func (lc *ListContext) GetOptionsMap() map[string]string {
if lc.OnGetOptionsMap != nil {
return lc.OnGetOptionsMap()
}
return nil
}
// OnFocus assumes that the content of the context has already been rendered to the view. OnRender is the function which actually renders the content to the view
func (lc *ListContext) OnRender() error {
view, err := lc.Gui.g.View(lc.ViewName)
@@ -103,18 +59,6 @@ func (lc *ListContext) OnRender() error {
return nil
}
func (lc *ListContext) GetKey() ContextKey {
return lc.ContextKey
}
func (lc *ListContext) GetKind() ContextKind {
return lc.Kind
}
func (lc *ListContext) GetViewName() string {
return lc.ViewName
}
func (lc *ListContext) HandleFocusLost() error {
if lc.OnFocusLost != nil {
return lc.OnFocusLost()
@@ -252,349 +196,3 @@ func (lc *ListContext) onSearchSelect(selectedLineIdx int) error {
lc.GetPanelState().SetSelectedLineIdx(selectedLineIdx)
return lc.HandleFocus()
}
func (gui *Gui) menuListContext() *ListContext {
return &ListContext{
ViewName: "menu",
ContextKey: "menu",
GetItemsLength: func() int { return gui.Views.Menu.LinesHeight() },
GetPanelState: func() IListPanelState { return gui.State.Panels.Menu },
OnFocus: gui.handleMenuSelect,
OnClickSelectedItem: gui.onMenuPress,
Gui: gui,
ResetMainViewOriginOnFocus: false,
Kind: PERSISTENT_POPUP,
OnGetOptionsMap: gui.getMenuOptions,
// no GetDisplayStrings field because we do a custom render on menu creation
}
}
func (gui *Gui) filesListContext() *ListContext {
return &ListContext{
ViewName: "files",
ContextKey: FILES_CONTEXT_KEY,
GetItemsLength: func() int { return gui.State.FileManager.GetItemsLength() },
GetPanelState: func() IListPanelState { return gui.State.Panels.Files },
OnFocus: gui.focusAndSelectFile,
OnClickSelectedItem: gui.handleFilePress,
Gui: gui,
ResetMainViewOriginOnFocus: false,
Kind: SIDE_CONTEXT,
GetDisplayStrings: func() [][]string {
lines := gui.State.FileManager.Render(gui.State.Modes.Diffing.Ref, gui.State.Submodules)
mappedLines := make([][]string, len(lines))
for i, line := range lines {
mappedLines[i] = []string{line}
}
return mappedLines
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedFileNode()
return item, item != nil
},
}
}
func (gui *Gui) branchesListContext() *ListContext {
return &ListContext{
ViewName: "branches",
ContextKey: LOCAL_BRANCHES_CONTEXT_KEY,
GetItemsLength: func() int { return len(gui.State.Branches) },
GetPanelState: func() IListPanelState { return gui.State.Panels.Branches },
OnFocus: gui.handleBranchSelect,
Gui: gui,
ResetMainViewOriginOnFocus: true,
Kind: SIDE_CONTEXT,
GetDisplayStrings: func() [][]string {
return presentation.GetBranchListDisplayStrings(gui.State.Branches, gui.State.ScreenMode != SCREEN_NORMAL, gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedBranch()
return item, item != nil
},
}
}
func (gui *Gui) remotesListContext() *ListContext {
return &ListContext{
ViewName: "branches",
ContextKey: REMOTES_CONTEXT_KEY,
GetItemsLength: func() int { return len(gui.State.Remotes) },
GetPanelState: func() IListPanelState { return gui.State.Panels.Remotes },
OnFocus: gui.handleRemoteSelect,
OnClickSelectedItem: gui.handleRemoteEnter,
Gui: gui,
ResetMainViewOriginOnFocus: true,
Kind: SIDE_CONTEXT,
GetDisplayStrings: func() [][]string {
return presentation.GetRemoteListDisplayStrings(gui.State.Remotes, gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedRemote()
return item, item != nil
},
}
}
func (gui *Gui) remoteBranchesListContext() *ListContext {
return &ListContext{
ViewName: "branches",
ContextKey: REMOTE_BRANCHES_CONTEXT_KEY,
GetItemsLength: func() int { return len(gui.State.RemoteBranches) },
GetPanelState: func() IListPanelState { return gui.State.Panels.RemoteBranches },
OnFocus: gui.handleRemoteBranchSelect,
Gui: gui,
ResetMainViewOriginOnFocus: true,
Kind: SIDE_CONTEXT,
GetDisplayStrings: func() [][]string {
return presentation.GetRemoteBranchListDisplayStrings(gui.State.RemoteBranches, gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedRemoteBranch()
return item, item != nil
},
}
}
func (gui *Gui) tagsListContext() *ListContext {
return &ListContext{
ViewName: "branches",
ContextKey: TAGS_CONTEXT_KEY,
GetItemsLength: func() int { return len(gui.State.Tags) },
GetPanelState: func() IListPanelState { return gui.State.Panels.Tags },
OnFocus: gui.handleTagSelect,
Gui: gui,
ResetMainViewOriginOnFocus: true,
Kind: SIDE_CONTEXT,
GetDisplayStrings: func() [][]string {
return presentation.GetTagListDisplayStrings(gui.State.Tags, gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedTag()
return item, item != nil
},
}
}
func (gui *Gui) branchCommitsListContext() *ListContext {
return &ListContext{
ViewName: "commits",
ContextKey: BRANCH_COMMITS_CONTEXT_KEY,
GetItemsLength: func() int { return len(gui.State.Commits) },
GetPanelState: func() IListPanelState { return gui.State.Panels.Commits },
OnFocus: gui.handleCommitSelect,
OnClickSelectedItem: gui.handleViewCommitFiles,
Gui: gui,
ResetMainViewOriginOnFocus: true,
Kind: SIDE_CONTEXT,
GetDisplayStrings: func() [][]string {
return presentation.GetCommitListDisplayStrings(gui.State.Commits, gui.State.ScreenMode != SCREEN_NORMAL, gui.cherryPickedCommitShaMap(), gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedLocalCommit()
return item, item != nil
},
}
}
func (gui *Gui) reflogCommitsListContext() *ListContext {
return &ListContext{
ViewName: "commits",
ContextKey: REFLOG_COMMITS_CONTEXT_KEY,
GetItemsLength: func() int { return len(gui.State.FilteredReflogCommits) },
GetPanelState: func() IListPanelState { return gui.State.Panels.ReflogCommits },
OnFocus: gui.handleReflogCommitSelect,
Gui: gui,
ResetMainViewOriginOnFocus: true,
Kind: SIDE_CONTEXT,
GetDisplayStrings: func() [][]string {
return presentation.GetReflogCommitListDisplayStrings(gui.State.FilteredReflogCommits, gui.State.ScreenMode != SCREEN_NORMAL, gui.cherryPickedCommitShaMap(), gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedReflogCommit()
return item, item != nil
},
}
}
func (gui *Gui) subCommitsListContext() *ListContext {
return &ListContext{
ViewName: "branches",
ContextKey: SUB_COMMITS_CONTEXT_KEY,
GetItemsLength: func() int { return len(gui.State.SubCommits) },
GetPanelState: func() IListPanelState { return gui.State.Panels.SubCommits },
OnFocus: gui.handleSubCommitSelect,
Gui: gui,
ResetMainViewOriginOnFocus: true,
Kind: SIDE_CONTEXT,
GetDisplayStrings: func() [][]string {
return presentation.GetCommitListDisplayStrings(gui.State.SubCommits, gui.State.ScreenMode != SCREEN_NORMAL, gui.cherryPickedCommitShaMap(), gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedSubCommit()
return item, item != nil
},
}
}
func (gui *Gui) stashListContext() *ListContext {
return &ListContext{
ViewName: "stash",
ContextKey: STASH_CONTEXT_KEY,
GetItemsLength: func() int { return len(gui.State.StashEntries) },
GetPanelState: func() IListPanelState { return gui.State.Panels.Stash },
OnFocus: gui.handleStashEntrySelect,
Gui: gui,
ResetMainViewOriginOnFocus: true,
Kind: SIDE_CONTEXT,
GetDisplayStrings: func() [][]string {
return presentation.GetStashEntryListDisplayStrings(gui.State.StashEntries, gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedStashEntry()
return item, item != nil
},
}
}
func (gui *Gui) commitFilesListContext() *ListContext {
return &ListContext{
ViewName: "commitFiles",
WindowName: "commits",
ContextKey: COMMIT_FILES_CONTEXT_KEY,
GetItemsLength: func() int { return gui.State.CommitFileManager.GetItemsLength() },
GetPanelState: func() IListPanelState { return gui.State.Panels.CommitFiles },
OnFocus: gui.handleCommitFileSelect,
Gui: gui,
ResetMainViewOriginOnFocus: true,
Kind: SIDE_CONTEXT,
GetDisplayStrings: func() [][]string {
if gui.State.CommitFileManager.GetItemsLength() == 0 {
return [][]string{{utils.ColoredString("(none)", color.FgRed)}}
}
lines := gui.State.CommitFileManager.Render(gui.State.Modes.Diffing.Ref, gui.GitCommand.PatchManager)
mappedLines := make([][]string, len(lines))
for i, line := range lines {
mappedLines[i] = []string{line}
}
return mappedLines
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedCommitFileNode()
return item, item != nil
},
}
}
func (gui *Gui) submodulesListContext() *ListContext {
return &ListContext{
ViewName: "files",
WindowName: "files",
ContextKey: SUBMODULES_CONTEXT_KEY,
GetItemsLength: func() int { return len(gui.State.Submodules) },
GetPanelState: func() IListPanelState { return gui.State.Panels.Submodules },
OnFocus: gui.handleSubmoduleSelect,
Gui: gui,
ResetMainViewOriginOnFocus: true,
Kind: SIDE_CONTEXT,
GetDisplayStrings: func() [][]string {
return presentation.GetSubmoduleListDisplayStrings(gui.State.Submodules)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedSubmodule()
return item, item != nil
},
}
}
func (gui *Gui) suggestionsListContext() *ListContext {
return &ListContext{
ViewName: "suggestions",
WindowName: "suggestions",
ContextKey: SUGGESTIONS_CONTEXT_KEY,
GetItemsLength: func() int { return len(gui.State.Suggestions) },
GetPanelState: func() IListPanelState { return gui.State.Panels.Suggestions },
OnFocus: func() error { return nil },
Gui: gui,
ResetMainViewOriginOnFocus: false,
Kind: PERSISTENT_POPUP,
GetDisplayStrings: func() [][]string {
return presentation.GetSuggestionListDisplayStrings(gui.State.Suggestions)
},
}
}
func (gui *Gui) getListContexts() []*ListContext {
return []*ListContext{
gui.State.Contexts.Menu,
gui.State.Contexts.Files,
gui.State.Contexts.Branches,
gui.State.Contexts.Remotes,
gui.State.Contexts.RemoteBranches,
gui.State.Contexts.Tags,
gui.State.Contexts.BranchCommits,
gui.State.Contexts.BranchCommits,
gui.State.Contexts.ReflogCommits,
gui.State.Contexts.SubCommits,
gui.State.Contexts.Stash,
gui.State.Contexts.CommitFiles,
gui.State.Contexts.Submodules,
gui.State.Contexts.Suggestions,
}
}
func (gui *Gui) getListContextKeyBindings() []*Binding {
bindings := make([]*Binding, 0)
keybindingConfig := gui.Config.GetUserConfig().Keybinding
for _, listContext := range gui.getListContexts() {
listContext := listContext
bindings = append(bindings, []*Binding{
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gui.getKey(keybindingConfig.Universal.PrevItemAlt), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gui.getKey(keybindingConfig.Universal.PrevItem), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gocui.MouseWheelUp, Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gui.getKey(keybindingConfig.Universal.NextItemAlt), Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gui.getKey(keybindingConfig.Universal.NextItem), Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gui.getKey(keybindingConfig.Universal.PrevPage), Modifier: gocui.ModNone, Handler: listContext.handlePrevPage, Description: gui.Tr.LcPrevPage},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gui.getKey(keybindingConfig.Universal.NextPage), Modifier: gocui.ModNone, Handler: listContext.handleNextPage, Description: gui.Tr.LcNextPage},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gui.getKey(keybindingConfig.Universal.GotoTop), Modifier: gocui.ModNone, Handler: listContext.handleGotoTop, Description: gui.Tr.LcGotoTop},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.ContextKey)}, Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
{ViewName: listContext.ViewName, Contexts: []string{string(listContext.ContextKey)}, Key: gocui.MouseLeft, Modifier: gocui.ModNone, Handler: listContext.handleClick},
}...)
// the commits panel needs to lazyload things so it has a couple of its own handlers
openSearchHandler := gui.handleOpenSearch
gotoBottomHandler := listContext.handleGotoBottom
if listContext.ViewName == "commits" {
openSearchHandler = gui.handleOpenSearchForCommitsPanel
gotoBottomHandler = gui.handleGotoBottomForCommitsPanel
}
bindings = append(bindings, []*Binding{
{
ViewName: listContext.ViewName,
Contexts: []string{string(listContext.ContextKey)},
Key: gui.getKey(keybindingConfig.Universal.StartSearch),
Handler: func() error { return openSearchHandler(listContext.ViewName) },
Description: gui.Tr.LcStartSearch,
Tag: "navigation",
},
{
ViewName: listContext.ViewName,
Contexts: []string{string(listContext.ContextKey)},
Key: gui.getKey(keybindingConfig.Universal.GotoBottom),
Handler: gotoBottomHandler,
Description: gui.Tr.LcGotoBottom,
Tag: "navigation",
},
}...)
}
return bindings
}
+389
View File
@@ -0,0 +1,389 @@
package gui
import (
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (gui *Gui) menuListContext() *ListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "menu",
Key: "menu",
Kind: PERSISTENT_POPUP,
OnGetOptionsMap: gui.getMenuOptions,
},
GetItemsLength: func() int { return gui.Views.Menu.LinesHeight() },
GetPanelState: func() IListPanelState { return gui.State.Panels.Menu },
OnFocus: gui.handleMenuSelect,
OnClickSelectedItem: gui.onMenuPress,
Gui: gui,
ResetMainViewOriginOnFocus: false,
// no GetDisplayStrings field because we do a custom render on menu creation
}
}
func (gui *Gui) filesListContext() *ListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "files",
WindowName: "files",
Key: FILES_CONTEXT_KEY,
Kind: SIDE_CONTEXT,
},
GetItemsLength: func() int { return gui.State.FileManager.GetItemsLength() },
GetPanelState: func() IListPanelState { return gui.State.Panels.Files },
OnFocus: gui.focusAndSelectFile,
OnClickSelectedItem: gui.handleFilePress,
Gui: gui,
ResetMainViewOriginOnFocus: false,
GetDisplayStrings: func() [][]string {
lines := gui.State.FileManager.Render(gui.State.Modes.Diffing.Ref, gui.State.Submodules)
mappedLines := make([][]string, len(lines))
for i, line := range lines {
mappedLines[i] = []string{line}
}
return mappedLines
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedFileNode()
return item, item != nil
},
}
}
func (gui *Gui) branchesListContext() *ListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "branches",
WindowName: "branches",
Key: LOCAL_BRANCHES_CONTEXT_KEY,
Kind: SIDE_CONTEXT,
},
GetItemsLength: func() int { return len(gui.State.Branches) },
GetPanelState: func() IListPanelState { return gui.State.Panels.Branches },
OnFocus: gui.handleBranchSelect,
Gui: gui,
ResetMainViewOriginOnFocus: true,
GetDisplayStrings: func() [][]string {
return presentation.GetBranchListDisplayStrings(gui.State.Branches, gui.State.ScreenMode != SCREEN_NORMAL, gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedBranch()
return item, item != nil
},
}
}
func (gui *Gui) remotesListContext() *ListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "branches",
WindowName: "branches",
Key: REMOTES_CONTEXT_KEY,
Kind: SIDE_CONTEXT,
},
GetItemsLength: func() int { return len(gui.State.Remotes) },
GetPanelState: func() IListPanelState { return gui.State.Panels.Remotes },
OnFocus: gui.handleRemoteSelect,
OnClickSelectedItem: gui.handleRemoteEnter,
Gui: gui,
ResetMainViewOriginOnFocus: true,
GetDisplayStrings: func() [][]string {
return presentation.GetRemoteListDisplayStrings(gui.State.Remotes, gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedRemote()
return item, item != nil
},
}
}
func (gui *Gui) remoteBranchesListContext() *ListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "branches",
WindowName: "branches",
Key: REMOTE_BRANCHES_CONTEXT_KEY,
Kind: SIDE_CONTEXT,
},
GetItemsLength: func() int { return len(gui.State.RemoteBranches) },
GetPanelState: func() IListPanelState { return gui.State.Panels.RemoteBranches },
OnFocus: gui.handleRemoteBranchSelect,
Gui: gui,
ResetMainViewOriginOnFocus: true,
GetDisplayStrings: func() [][]string {
return presentation.GetRemoteBranchListDisplayStrings(gui.State.RemoteBranches, gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedRemoteBranch()
return item, item != nil
},
}
}
func (gui *Gui) tagsListContext() *ListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "branches",
WindowName: "branches",
Key: TAGS_CONTEXT_KEY,
Kind: SIDE_CONTEXT,
},
GetItemsLength: func() int { return len(gui.State.Tags) },
GetPanelState: func() IListPanelState { return gui.State.Panels.Tags },
OnFocus: gui.handleTagSelect,
Gui: gui,
ResetMainViewOriginOnFocus: true,
GetDisplayStrings: func() [][]string {
return presentation.GetTagListDisplayStrings(gui.State.Tags, gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedTag()
return item, item != nil
},
}
}
func (gui *Gui) branchCommitsListContext() *ListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "commits",
WindowName: "commits",
Key: BRANCH_COMMITS_CONTEXT_KEY,
Kind: SIDE_CONTEXT,
},
GetItemsLength: func() int { return len(gui.State.Commits) },
GetPanelState: func() IListPanelState { return gui.State.Panels.Commits },
OnFocus: gui.handleCommitSelect,
OnClickSelectedItem: gui.handleViewCommitFiles,
Gui: gui,
ResetMainViewOriginOnFocus: true,
GetDisplayStrings: func() [][]string {
return presentation.GetCommitListDisplayStrings(gui.State.Commits, gui.State.ScreenMode != SCREEN_NORMAL, gui.cherryPickedCommitShaMap(), gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedLocalCommit()
return item, item != nil
},
}
}
func (gui *Gui) reflogCommitsListContext() *ListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "commits",
WindowName: "commits",
Key: REFLOG_COMMITS_CONTEXT_KEY,
Kind: SIDE_CONTEXT,
},
GetItemsLength: func() int { return len(gui.State.FilteredReflogCommits) },
GetPanelState: func() IListPanelState { return gui.State.Panels.ReflogCommits },
OnFocus: gui.handleReflogCommitSelect,
Gui: gui,
ResetMainViewOriginOnFocus: true,
GetDisplayStrings: func() [][]string {
return presentation.GetReflogCommitListDisplayStrings(gui.State.FilteredReflogCommits, gui.State.ScreenMode != SCREEN_NORMAL, gui.cherryPickedCommitShaMap(), gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedReflogCommit()
return item, item != nil
},
}
}
func (gui *Gui) subCommitsListContext() *ListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "branches",
WindowName: "branches",
Key: SUB_COMMITS_CONTEXT_KEY,
Kind: SIDE_CONTEXT,
},
GetItemsLength: func() int { return len(gui.State.SubCommits) },
GetPanelState: func() IListPanelState { return gui.State.Panels.SubCommits },
OnFocus: gui.handleSubCommitSelect,
Gui: gui,
ResetMainViewOriginOnFocus: true,
GetDisplayStrings: func() [][]string {
return presentation.GetCommitListDisplayStrings(gui.State.SubCommits, gui.State.ScreenMode != SCREEN_NORMAL, gui.cherryPickedCommitShaMap(), gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedSubCommit()
return item, item != nil
},
}
}
func (gui *Gui) stashListContext() *ListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "stash",
WindowName: "stash",
Key: STASH_CONTEXT_KEY,
Kind: SIDE_CONTEXT,
},
GetItemsLength: func() int { return len(gui.State.StashEntries) },
GetPanelState: func() IListPanelState { return gui.State.Panels.Stash },
OnFocus: gui.handleStashEntrySelect,
Gui: gui,
ResetMainViewOriginOnFocus: true,
GetDisplayStrings: func() [][]string {
return presentation.GetStashEntryListDisplayStrings(gui.State.StashEntries, gui.State.Modes.Diffing.Ref)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedStashEntry()
return item, item != nil
},
}
}
func (gui *Gui) commitFilesListContext() *ListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "commitFiles",
WindowName: "commits",
Key: COMMIT_FILES_CONTEXT_KEY,
Kind: SIDE_CONTEXT,
},
GetItemsLength: func() int { return gui.State.CommitFileManager.GetItemsLength() },
GetPanelState: func() IListPanelState { return gui.State.Panels.CommitFiles },
OnFocus: gui.handleCommitFileSelect,
Gui: gui,
ResetMainViewOriginOnFocus: true,
GetDisplayStrings: func() [][]string {
if gui.State.CommitFileManager.GetItemsLength() == 0 {
return [][]string{{utils.ColoredString("(none)", color.FgRed)}}
}
lines := gui.State.CommitFileManager.Render(gui.State.Modes.Diffing.Ref, gui.GitCommand.PatchManager)
mappedLines := make([][]string, len(lines))
for i, line := range lines {
mappedLines[i] = []string{line}
}
return mappedLines
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedCommitFileNode()
return item, item != nil
},
}
}
func (gui *Gui) submodulesListContext() *ListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "files",
WindowName: "files",
Key: SUBMODULES_CONTEXT_KEY,
Kind: SIDE_CONTEXT,
},
GetItemsLength: func() int { return len(gui.State.Submodules) },
GetPanelState: func() IListPanelState { return gui.State.Panels.Submodules },
OnFocus: gui.handleSubmoduleSelect,
Gui: gui,
ResetMainViewOriginOnFocus: true,
GetDisplayStrings: func() [][]string {
return presentation.GetSubmoduleListDisplayStrings(gui.State.Submodules)
},
SelectedItem: func() (ListItem, bool) {
item := gui.getSelectedSubmodule()
return item, item != nil
},
}
}
func (gui *Gui) suggestionsListContext() *ListContext {
return &ListContext{
BasicContext: &BasicContext{
ViewName: "suggestions",
WindowName: "suggestions",
Key: SUGGESTIONS_CONTEXT_KEY,
Kind: PERSISTENT_POPUP,
},
GetItemsLength: func() int { return len(gui.State.Suggestions) },
GetPanelState: func() IListPanelState { return gui.State.Panels.Suggestions },
OnFocus: func() error { return nil },
Gui: gui,
ResetMainViewOriginOnFocus: false,
GetDisplayStrings: func() [][]string {
return presentation.GetSuggestionListDisplayStrings(gui.State.Suggestions)
},
}
}
func (gui *Gui) getListContexts() []*ListContext {
return []*ListContext{
gui.State.Contexts.Menu,
gui.State.Contexts.Files,
gui.State.Contexts.Branches,
gui.State.Contexts.Remotes,
gui.State.Contexts.RemoteBranches,
gui.State.Contexts.Tags,
gui.State.Contexts.BranchCommits,
gui.State.Contexts.BranchCommits,
gui.State.Contexts.ReflogCommits,
gui.State.Contexts.SubCommits,
gui.State.Contexts.Stash,
gui.State.Contexts.CommitFiles,
gui.State.Contexts.Submodules,
gui.State.Contexts.Suggestions,
}
}
func (gui *Gui) getListContextKeyBindings() []*Binding {
bindings := make([]*Binding, 0)
keybindingConfig := gui.Config.GetUserConfig().Keybinding
for _, listContext := range gui.getListContexts() {
listContext := listContext
bindings = append(bindings, []*Binding{
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gui.getKey(keybindingConfig.Universal.PrevItemAlt), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gui.getKey(keybindingConfig.Universal.PrevItem), Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gocui.MouseWheelUp, Modifier: gocui.ModNone, Handler: listContext.handlePrevLine},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gui.getKey(keybindingConfig.Universal.NextItemAlt), Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gui.getKey(keybindingConfig.Universal.NextItem), Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gui.getKey(keybindingConfig.Universal.PrevPage), Modifier: gocui.ModNone, Handler: listContext.handlePrevPage, Description: gui.Tr.LcPrevPage},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gui.getKey(keybindingConfig.Universal.NextPage), Modifier: gocui.ModNone, Handler: listContext.handleNextPage, Description: gui.Tr.LcNextPage},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gui.getKey(keybindingConfig.Universal.GotoTop), Modifier: gocui.ModNone, Handler: listContext.handleGotoTop, Description: gui.Tr.LcGotoTop},
{ViewName: listContext.ViewName, Tag: "navigation", Contexts: []string{string(listContext.Key)}, Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, Handler: listContext.handleNextLine},
{ViewName: listContext.ViewName, Contexts: []string{string(listContext.Key)}, Key: gocui.MouseLeft, Modifier: gocui.ModNone, Handler: listContext.handleClick},
}...)
// the commits panel needs to lazyload things so it has a couple of its own handlers
openSearchHandler := gui.handleOpenSearch
gotoBottomHandler := listContext.handleGotoBottom
if listContext.ViewName == "commits" {
openSearchHandler = gui.handleOpenSearchForCommitsPanel
gotoBottomHandler = gui.handleGotoBottomForCommitsPanel
}
bindings = append(bindings, []*Binding{
{
ViewName: listContext.ViewName,
Contexts: []string{string(listContext.Key)},
Key: gui.getKey(keybindingConfig.Universal.StartSearch),
Handler: func() error { return openSearchHandler(listContext.ViewName) },
Description: gui.Tr.LcStartSearch,
Tag: "navigation",
},
{
ViewName: listContext.ViewName,
Contexts: []string{string(listContext.Key)},
Key: gui.getKey(keybindingConfig.Universal.GotoBottom),
Handler: gotoBottomHandler,
Description: gui.Tr.LcGotoBottom,
Tag: "navigation",
},
}...)
}
return bindings
}
+5 -1
View File
@@ -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
View File
@@ -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()
}
-86
View File
@@ -1,86 +0,0 @@
package mergeconflicts
import (
"bytes"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type Selection int
const (
TOP Selection = iota
BOTTOM
BOTH
)
func FindConflicts(content string) []commands.Conflict {
conflicts := make([]commands.Conflict, 0)
if content == "" {
return conflicts
}
var newConflict commands.Conflict
for i, line := range utils.SplitLines(content) {
trimmedLine := strings.TrimPrefix(line, "++")
switch trimmedLine {
case "<<<<<<< HEAD", "<<<<<<< MERGE_HEAD", "<<<<<<< Updated upstream", "<<<<<<< ours":
newConflict = commands.Conflict{Start: i}
case "=======":
newConflict.Middle = i
default:
if strings.HasPrefix(trimmedLine, ">>>>>>> ") {
newConflict.End = i
conflicts = append(conflicts, newConflict)
}
}
}
return conflicts
}
func ColoredConflictFile(content string, conflicts []commands.Conflict, conflictIndex int, conflictTop, hasFocus bool) string {
if len(conflicts) == 0 {
return content
}
conflict, remainingConflicts := shiftConflict(conflicts)
var outputBuffer bytes.Buffer
for i, line := range utils.SplitLines(content) {
colourAttr := theme.DefaultTextColor
if i == conflict.Start || i == conflict.Middle || i == conflict.End {
colourAttr = color.FgRed
}
colour := color.New(colourAttr)
if hasFocus && conflictIndex < len(conflicts) && conflicts[conflictIndex] == conflict && shouldHighlightLine(i, conflict, conflictTop) {
colour.Add(color.Bold)
colour.Add(theme.SelectedRangeBgColor)
}
if i == conflict.End && len(remainingConflicts) > 0 {
conflict, remainingConflicts = shiftConflict(remainingConflicts)
}
outputBuffer.WriteString(utils.ColoredStringDirect(line, colour) + "\n")
}
return outputBuffer.String()
}
func IsIndexToDelete(i int, conflict commands.Conflict, selection Selection) bool {
return i == conflict.Middle ||
i == conflict.Start ||
i == conflict.End ||
selection != BOTH &&
(selection == BOTTOM && i > conflict.Start && i < conflict.Middle) ||
(selection == TOP && i > conflict.Middle && i < conflict.End)
}
func shiftConflict(conflicts []commands.Conflict) (commands.Conflict, []commands.Conflict) {
return conflicts[0], conflicts[1:]
}
func shouldHighlightLine(index int, conflict commands.Conflict, top bool) bool {
return (index >= conflict.Start && index <= conflict.Middle && top) || (index >= conflict.Middle && index <= conflict.End && !top)
}
+41
View File
@@ -0,0 +1,41 @@
package mergeconflicts
import (
"bytes"
"github.com/fatih/color"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func ColoredConflictFile(content string, state *State, hasFocus bool) string {
if len(state.conflicts) == 0 {
return content
}
conflict, remainingConflicts := shiftConflict(state.conflicts)
var outputBuffer bytes.Buffer
for i, line := range utils.SplitLines(content) {
colourAttr := theme.DefaultTextColor
if i == conflict.start || i == conflict.middle || i == conflict.end {
colourAttr = color.FgRed
}
colour := color.New(colourAttr)
if hasFocus && state.conflictIndex < len(state.conflicts) && *state.conflicts[state.conflictIndex] == *conflict && shouldHighlightLine(i, conflict, state.conflictTop) {
colour.Add(color.Bold)
colour.Add(theme.SelectedRangeBgColor)
}
if i == conflict.end && len(remainingConflicts) > 0 {
conflict, remainingConflicts = shiftConflict(remainingConflicts)
}
outputBuffer.WriteString(utils.ColoredStringDirect(line, colour) + "\n")
}
return outputBuffer.String()
}
func shiftConflict(conflicts []*mergeConflict) (*mergeConflict, []*mergeConflict) {
return conflicts[0], conflicts[1:]
}
func shouldHighlightLine(index int, conflict *mergeConflict, top bool) bool {
return (index >= conflict.start && index <= conflict.middle && top) || (index >= conflict.middle && index <= conflict.end && !top)
}
+185
View File
@@ -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)
}
+7 -5
View File
@@ -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)
}
+13 -9
View File
@@ -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})
+4 -2
View File
@@ -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
}
+1 -1
View File
@@ -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 {
+2 -2
View File
@@ -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
}
+6 -4
View File
@@ -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}})
+3 -3
View File
@@ -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{})
},
}
}
-1
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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}})
+7 -6
View File
@@ -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{
+1 -1
View File
@@ -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
View File
@@ -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}})
+4 -4
View File
@@ -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
View File
@@ -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
+6 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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}}?`,
+3 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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