add integration test for staging view

This commit is contained in:
Jesse Duffield 2022-01-17 17:55:44 +11:00
parent 99035959a1
commit ee622d044e
37 changed files with 1102 additions and 0 deletions

View file

@ -0,0 +1 @@
test

View file

@ -0,0 +1 @@
ref: refs/heads/master

View file

@ -0,0 +1,10 @@
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[user]
email = CI@example.com
name = CI

View file

@ -0,0 +1 @@
Unnamed repository; edit this file 'description' to name the repository.

Binary file not shown.

View file

@ -0,0 +1,7 @@
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~
.DS_Store

View file

@ -0,0 +1,2 @@
0000000000000000000000000000000000000000 6806c569c7c063ec7ed3d16da3faa32a1622bb4b CI <CI@example.com> 1642402468 +1100 commit (initial): file1
6806c569c7c063ec7ed3d16da3faa32a1622bb4b 4253d2597aec2a480a8e9054250e6b4aa5b76d9e CI <CI@example.com> 1642402495 +1100 commit: test

View file

@ -0,0 +1,2 @@
0000000000000000000000000000000000000000 6806c569c7c063ec7ed3d16da3faa32a1622bb4b CI <CI@example.com> 1642402468 +1100 commit (initial): file1
6806c569c7c063ec7ed3d16da3faa32a1622bb4b 4253d2597aec2a480a8e9054250e6b4aa5b76d9e CI <CI@example.com> 1642402495 +1100 commit: test

View file

@ -0,0 +1,2 @@
x<01>ÎA
Â0@Q×9EödÌ4<C38C>"BW=ÆL:AÁØR#x|{·Ÿ·øemíÑ-d8õ]Õ:̘H"%IYjN„<xªP”#c5ïú꣔ËP-ƒ.aZ8TæàÈ{(†?ý¾îvší8Í7ýrÛžz)k»Z è£ó1£=8gŽzLuý“®ïn~!9o

View file

@ -0,0 +1,4 @@
xM±nÜ0 †;û)þN·øŒtÉÐN:dêÈÐY¶hK8Yt%ê„ÛúyÂ<I()
†Eþ"?þôxÆã—‡O—"G‰FT:ÝÕ„+Yü.^m¸£Æ2âFœÏÃ&Zh”"ªç÷ sè‰9hºëŸM²µ®óùð!ø¸a§œÏâc†<63>ààT? ý¢;e%:dĬˆ-¹èËlëm< jÒ†4 Otj%HŸ¢—Ç\B É<>Â5<C382>ÙáÅ%sD
~Ç©d¥ªNp„l”Ê^e4_•VBÏû@]àÌMÏmú4˽s µ¦;¤fDºŽi¿ƒ×6@þÜjÓAl7«jˆÌVZ—
WöiøÉÈzò”tˆÃxÛöpõËf3êœàI=¸RÊÓõG¼ýyÅ5rmFÓv3‰(ÒÞvÅ¥uà<75>Æá»npKm‡IÁSþú× 2øX¬~s-;«ýYýî2püøVNþFù"Kk0¢ÅÖÐôï‡8Öï

View file

@ -0,0 +1,2 @@
x<01>ÍA
Â0Fa×9Åì™I~B"BW=Æ$<24>`¡!R"x|{·<>­¸ŒÃŒj6N*BÈ€i䌘W®a¾ä(SR8ýŒW?h^è>/Oûj{ïv+½=H"<Ø#Ntavg='Ãþä®n»‰û¶¹+•

View file

@ -0,0 +1,2 @@
xM¿nÜ0 Æ;û)¾N·øŒNš©@‡L¹m '®D<C2AE>p["O˜')¥4ECÿˆüñ£çÀ3¾<|º<14>8J4¢ÒéF¨&\ÉâWñBX“§hÃå€0¾“7â|^ŒÀr<C380>0ÑB½Q½88¿<38>X˜CÌAÃ=ÿ‡I£Öu>#>7ì”óY|Ìð\œæOÃEot§¬D‡Œ˜±=ÌF°ÞÆ“ &mHÓðD§VR<56>ô+ú¨qÌ%’Ü)\KÐÇ#;¼¸dŽHaÂOB6
Q<EFBFBD>»¿† ¼Ê8h¬*©%ÖK žàÌMí<4D>}’åÞÊÒÕQý2"ÝHGˆ´ßÁkƒÏŸ[Í`:DíBUuÙJëòO=+û4<3²Zžp®~¹ÂlFU<©šWJyú¯þˆ·ß¯¸F®MdqÚn&EÚÛž¸´¼Ó8|Óím©í/)xÊ_ßÕ ³8Åê7×¢³JŸU랎ÿÂÊ Áß(?"²´#šo -ÿ΢Õ@

View file

@ -0,0 +1 @@
x==ŽÜ0 …Sű/Ő4Ţ©-*@Š­vl±µlŃ0˛ččgŚér<C3A9>ś0'É“ ň‰üřč)č„çç/ź.µ 8I2â<32>ÓMp<4D>pÁĽDî¨;Šâ‡<C3A2>âF<= o¦ŔęŁqřâŕü6bV =1¦»ţ§IV#ë:źw‚Ź+6Éů©ř<C2A9>á#´&8ęĎĂ{ÉŃ/ _<>kv(ÎĎW¨mJ]z©)} ]•»dα—k™‡YÖŰx*81ĺ<ĽČ©<C48C>p ~•ŹýTC<54>;»k>^ťŘáÍ%łG gĽ f§©fŽP¤Ë†ł<E280A0> KÉΰĎ%ôĽŇÎÜx´éĚ÷Î1\hh÷•ÎgDą Ç<>˛ÝŰ<C39D>dĎź[Í`:H<>í ‰Ykëňá»K¸şť‡WEćÍKâ»ńtQqmî™ŐĐďîa×CRn~?ęŹřűű®QʶžâŘnR<E28099>´µ kmt“qřνs+Ü|"xĘ_˙»!fvŘ“VĹŻ®e'Úźéw—Aăă/Z4!řäo<C3A4>ZZ<5A>-¶„¦˙ Îéb

View file

@ -0,0 +1 @@
4253d2597aec2a480a8e9054250e6b4aa5b76d9e

View file

@ -0,0 +1,15 @@
Out there, we've walked quite friendly up to Death, --
Sat down and eaten with him, cool and bland, --
Pardoned his spilling mess-tins in our hand.
We've sniffed the green thick smell of his breath, --
Our eyes wept, but our courage did not writhe.
He's spat at us with bullets and he's coughed
Shrapnel. We sang when he sang aloft,
We whistled while he shaved us with his scythe.
Oh, Death was never enemy of ours!
We laughed at him, we leagued with him, old chum.
No soldier's paid to kick against His powers.
We laughed, — knowing that greater men would come,
And greater wars: when each proud fighter brags
He wars on Death, for lives; not men, for flags

View file

@ -0,0 +1,301 @@
package gui
import (
"fmt"
"regexp"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// list panel functions
func (gui *Gui) getSelectednodeNode() *nodetree.nodeNode {
selectedLine := gui.State.Panels.nodes.SelectedLineIdx
if selectedLine == -1 {
return nil
}
return gui.State.FileManager.GetItemAtIndex(selectedLine)
return gui.State.nodeManager.GetItemAtIndex(selectedLine)
}
func (gui *Gui) getSelectednode() *models.node {
node := gui.getSelectednodeNode()
if node == nil {
return nil
}
return node.node
}
func (gui *Gui) getSelectedPath() string {
node := gui.getSelectednodeNode()
if node == nil {
return ""
}
return node.GetPath()
}
func (gui *Gui) filesRenderToMain() error {
node := gui.getSelectedFileNode()
func (gui *Gui) nodesRenderToMain() error {
node := gui.getSelectednodeNode()
if node == nil {
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: "",
task: NewRenderStringTask(gui.Tr.NoChangednodes),
},
})
}
if node.node != nil && node.File.HasInlineMergeConflicts {
return gui.renderConflictsFromFilesPanel()
}
cmdObj := gui.Git.WorkingTree.WorktreeFileDiffCmdObj(node, false, !node.GetHasUnstagedChanges() && node.GetHasStagedChanges(), gui.State.IgnoreWhitespaceInDiffView)
refreshOpts := refreshMainOpts{main: &viewUpdateOpts{
title: gui.Tr.UnstagedChanges,
task: NewRunPtyTask(cmdObj.GetCmd()),
}}
if node.GetHasUnstagedChanges() {
if node.GetHasStagedChanges() {
cmdObj := gui.Git.WorkingTree.WorktreeFileDiffCmdObj(node, false, true, gui.State.IgnoreWhitespaceInDiffView)
refreshOpts.secondary = &viewUpdateOpts{
title: gui.Tr.StagedChanges,
task: NewRunPtyTask(cmdObj.GetCmd()),
}
}
} else {
refreshOpts.main.title = gui.Tr.StagedChanges
}
return gui.refreshMainViews(refreshOpts)
}
func (gui *Gui) refreshFilesAndSubmodules() error {
gui.Mutexes.RefreshingFilesMutex.Lock()
gui.State.IsRefreshingFiles = true
defer func() {
gui.State.IsRefreshingFiles = false
gui.Mutexes.RefreshingFilesMutex.Unlock()
}()
selectedPath := gui.getSelectedPath()
if err := gui.refreshStateSubmoduleConfigs(); err != nil {
return err
}
if err := gui.refreshStateFiles(); err != nil {
return err
}
gui.OnUIThread(func() error {
if err := gui.postRefreshUpdate(gui.State.Contexts.Submodules); err != nil {
gui.Log.Error(err)
}
if ContextKey(gui.Views.Files.Context) == FILES_CONTEXT_KEY {
// doing this a little custom (as opposed to using gui.postRefreshUpdate) because we handle selecting the file explicitly below
if err := gui.State.Contexts.Files.HandleRender(); err != nil {
return err
}
}
if gui.currentContext().GetKey() == FILES_CONTEXT_KEY || (gui.g.CurrentView() == gui.Views.Main && ContextKey(gui.g.CurrentView().Context) == MAIN_MERGING_CONTEXT_KEY) {
newSelectedPath := gui.getSelectedPath()
alreadySelected := selectedPath != "" && newSelectedPath == selectedPath
if !alreadySelected {
gui.takeOverMergeConflictScrolling()
}
gui.Views.Files.FocusPoint(0, gui.State.Panels.Files.SelectedLineIdx)
return gui.filesRenderToMain()
}
return nil
})
return nil
}
// specific functions
func (gui *Gui) stagedFiles() []*models.File {
files := gui.State.FileManager.GetAllFiles()
result := make([]*models.File, 0)
for _, file := range files {
if file.HasStagedChanges {
result = append(result, file)
}
}
return result
}
func (gui *Gui) trackedFiles() []*models.File {
files := gui.State.FileManager.GetAllFiles()
result := make([]*models.File, 0, len(files))
for _, file := range files {
if file.Tracked {
result = append(result, file)
}
}
return result
}
func (gui *Gui) stageSelectedFile() error {
file := gui.getSelectedFile()
if file == nil {
return nil
}
return gui.Git.WorkingTree.StageFile(file.Name)
}
func (gui *Gui) handleEnterFile() error {
return gui.enterFile(OnFocusOpts{ClickedViewName: "", ClickedViewLineIdx: -1})
}
func (gui *Gui) enterFile(opts OnFocusOpts) error {
node := gui.getSelectedFileNode()
if node == nil {
return nil
}
if node.File == nil {
return gui.handleToggleDirCollapsed()
}
file := node.File
submoduleConfigs := gui.State.Submodules
if file.IsSubmodule(submoduleConfigs) {
submoduleConfig := file.SubmoduleConfig(submoduleConfigs)
return gui.enterSubmodule(submoduleConfig)
}
if file.HasInlineMergeConflicts {
return gui.switchToMerge()
}
if file.HasMergeConflicts {
return gui.createErrorPanel(gui.Tr.FileStagingRequirements)
}
return gui.pushContext(gui.State.Contexts.Staging, opts)
}
func (gui *Gui) handleFilePress() error {
node := gui.getSelectedFileNode()
if node == nil {
return nil
}
if node.IsLeaf() {
file := node.File
if file.HasInlineMergeConflicts {
return gui.switchToMerge()
}
if node.HasUnstagedChanges {
gui.logAction(gui.Tr.Actions.Stagenode)
if err := gui.Git.WorkingTree.Stagenode(node.Name); err != nil {
return gui.surfaceError(err)
}
} else {
gui.logAction(gui.Tr.Actions.Unstagenode)
if err := gui.Git.WorkingTree.UnStagenode(node.Names(), node.Tracked); err != nil {
return gui.surfaceError(err)
}
}
} else {
if node.GetHasInlineMergeConflicts() {
return gui.createErrorPanel(gui.Tr.ErrStageDirWithInlineMergeConflicts)
}
if node.GetHasUnstagedChanges() {
gui.logAction(gui.Tr.Actions.Stagenode)
if err := gui.Git.WorkingTree.Stagenode(node.Path); err != nil {
return gui.surfaceError(err)
}
} else {
// pretty sure it doesn't matter that we're always passing true here
gui.logAction(gui.Tr.Actions.Unstagenode)
if err := gui.Git.WorkingTree.UnStagenode([]string{node.Path}, true); err != nil {
return gui.surfaceError(err)
}
}
}
if err := gui.blah(refreshOptions{scope: []RefreshableView{nodeS}}); err != nil {
return err
}
return gui.State.Contexts.nodes.HandleFocus()
}
func (gui *Gui) allnodesStaged() bool {
for _, node := range gui.State.nodeManager.GetAllnodes() {
if node.HasUnstagedChanges {
return false
}
}
return true
}
func (gui *Gui) onFocusnode() error {
gui.takeOverMergeConflictScrolling()
return nil
}
func (gui *Gui) handleStageAll() error {
var err error
if gui.allnodesStaged() {
gui.logAction(gui.Tr.Actions.UnstageAllnodes)
err = gui.Git.WorkingTree.UnstageAll()
} else {
gui.logAction(gui.Tr.Actions.StageAllnodes)
err = gui.Git.WorkingTree.StageAll()
}
if err != nil {
_ = gui.surfaceError(err)
}
if err := gui.blah(refreshOptions{scope: []RefreshableView{nodeS}}); err != nil {
return err
}
return gui.State.Contexts.nodes.HandleFocus()
}
func (gui *Gui) handleIgnorenode() error {
node := gui.getSelectednodeNode()
if node == nil {
return nil
}
if node.GetPath() == ".gitignore" {
return gui.createErrorPanel("Cannot ignore .gitignore")
}
unstagenodes := func() error {
return node.ForEachnode(func(node *models.node) error {
if node.HasStagedChanges {
if err := gui.Git.WorkingTree.UnStagenode(node.Names(), node.Tracked); err != nil {
return err
}
}
return nil
})
}

View file

@ -0,0 +1,33 @@
type createMenuOptions struct {
showCancel bool
}
func (gui *Gui) createMenu(title string, items []*menuItem, createMenuOptions createMenuOptions) error {
if createMenuOptions.showCancel {
// this is mutative but I'm okay with that for now
items = app(items, &menuItem{
d: []string{gui.Tr.LcCancel},
onPress: func() error {
return nil
},
})
}
gui.State.MenuItems = items
stringArrays := make([][]string, len(items))
for i, items := range items {
if items.opensMenu && item.displayStrings != nil {
return errors.New("Message for the developer of this app: you've set opensMenu with displaystrings on the menu panel. Bad developer!. Apologies, user")
}
if item.displayStrings == nil {
styledStr := item.displayString
if item.opensMenu {
styledStr = opensMenuStyle(styledStr)
}
stringArrays[i] = []string{styledStr}
} else {
stringArrays[i] = item.displayStrings
}
}

View file

@ -0,0 +1,15 @@
Out there, we've walked quite friendly up to Death, --
Sat down and eaten with him, cool and bland, --
Pardoned his spilling mess-tins in our hand.
We've sniffed the green thick odour of his breath, --
Our eyes wept, but our courage didn't writhe.
He's spat at us with bullets and he's coughed
Shrapnel. We chorused when he sang aloft,
We whistled while he shaved us with his scythe.
Oh, Death was never enemy of ours!
We laughed at him, we leagued with him, old chum.
No soldier's paid to kick against His powers.
We laughed, — knowing that better men would come,
And greater wars: when each proud fighter brags
He wars on Death, for lives; not men, for flags

View file

@ -0,0 +1,15 @@
Out there, we've walked quite friendly up to Death, --
Sat down and eaten with him, cool and bland, --
Pardoned his spilling mess-tins in our hand.
We've sniffed the green thick smell of his breath, --
Our eyes wept, but our courage did not writhe.
He's spat at us with bullets and he's coughed
Shrapnel. We sang when he sang aloft,
We whistled while he shaved us with his scythe.
Oh, Death was never enemy of ours!
We laughed at him, we leagued with him, old chum.
No soldier's paid to kick against His powers.
We laughed, — knowing that greater men would come,
And greater wars: when each proud fighter brags
He wars on Death, for lives; not men, for flags

View file

@ -0,0 +1,300 @@
package gui
import (
"fmt"
"regexp"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// list panel functions
func (gui *Gui) getSelectedFileNode() *filetree.FileNode {
selectedLine := gui.State.Panels.Files.SelectedLineIdx
if selectedLine == -1 {
return nil
}
return gui.State.FileManager.GetItemAtIndex(selectedLine)
}
func (gui *Gui) getSelectedFile() *models.File {
node := gui.getSelectedFileNode()
if node == nil {
return nil
}
return node.File
}
func (gui *Gui) getSelectedPath() string {
node := gui.getSelectedFileNode()
if node == nil {
return ""
}
return node.GetPath()
}
func (gui *Gui) filesRenderToMain() error {
node := gui.getSelectedFileNode()
if node == nil {
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: "",
task: NewRenderStringTask(gui.Tr.NoChangedFiles),
},
})
}
if node.File != nil && node.File.HasInlineMergeConflicts {
return gui.renderConflictsFromFilesPanel()
}
cmdObj := gui.Git.WorkingTree.WorktreeFileDiffCmdObj(node, false, !node.GetHasUnstagedChanges() && node.GetHasStagedChanges(), gui.State.IgnoreWhitespaceInDiffView)
refreshOpts := refreshMainOpts{main: &viewUpdateOpts{
title: gui.Tr.UnstagedChanges,
task: NewRunPtyTask(cmdObj.GetCmd()),
}}
if node.GetHasUnstagedChanges() {
if node.GetHasStagedChanges() {
cmdObj := gui.Git.WorkingTree.WorktreeFileDiffCmdObj(node, false, true, gui.State.IgnoreWhitespaceInDiffView)
refreshOpts.secondary = &viewUpdateOpts{
title: gui.Tr.StagedChanges,
task: NewRunPtyTask(cmdObj.GetCmd()),
}
}
} else {
refreshOpts.main.title = gui.Tr.StagedChanges
}
return gui.refreshMainViews(refreshOpts)
}
func (gui *Gui) refreshFilesAndSubmodules() error {
gui.Mutexes.RefreshingFilesMutex.Lock()
gui.State.IsRefreshingFiles = true
defer func() {
gui.State.IsRefreshingFiles = false
gui.Mutexes.RefreshingFilesMutex.Unlock()
}()
selectedPath := gui.getSelectedPath()
if err := gui.refreshStateSubmoduleConfigs(); err != nil {
return err
}
if err := gui.refreshStateFiles(); err != nil {
return err
}
gui.OnUIThread(func() error {
if err := gui.postRefreshUpdate(gui.State.Contexts.Submodules); err != nil {
gui.Log.Error(err)
}
if ContextKey(gui.Views.Files.Context) == FILES_CONTEXT_KEY {
// doing this a little custom (as opposed to using gui.postRefreshUpdate) because we handle selecting the file explicitly below
if err := gui.State.Contexts.Files.HandleRender(); err != nil {
return err
}
}
if gui.currentContext().GetKey() == FILES_CONTEXT_KEY || (gui.g.CurrentView() == gui.Views.Main && ContextKey(gui.g.CurrentView().Context) == MAIN_MERGING_CONTEXT_KEY) {
newSelectedPath := gui.getSelectedPath()
alreadySelected := selectedPath != "" && newSelectedPath == selectedPath
if !alreadySelected {
gui.takeOverMergeConflictScrolling()
}
gui.Views.Files.FocusPoint(0, gui.State.Panels.Files.SelectedLineIdx)
return gui.filesRenderToMain()
}
return nil
})
return nil
}
// specific functions
func (gui *Gui) stagedFiles() []*models.File {
files := gui.State.FileManager.GetAllFiles()
result := make([]*models.File, 0)
for _, file := range files {
if file.HasStagedChanges {
result = append(result, file)
}
}
return result
}
func (gui *Gui) trackedFiles() []*models.File {
files := gui.State.FileManager.GetAllFiles()
result := make([]*models.File, 0, len(files))
for _, file := range files {
if file.Tracked {
result = append(result, file)
}
}
return result
}
func (gui *Gui) stageSelectedFile() error {
file := gui.getSelectedFile()
if file == nil {
return nil
}
return gui.Git.WorkingTree.StageFile(file.Name)
}
func (gui *Gui) handleEnterFile() error {
return gui.enterFile(OnFocusOpts{ClickedViewName: "", ClickedViewLineIdx: -1})
}
func (gui *Gui) enterFile(opts OnFocusOpts) error {
node := gui.getSelectedFileNode()
if node == nil {
return nil
}
if node.File == nil {
return gui.handleToggleDirCollapsed()
}
file := node.File
submoduleConfigs := gui.State.Submodules
if file.IsSubmodule(submoduleConfigs) {
submoduleConfig := file.SubmoduleConfig(submoduleConfigs)
return gui.enterSubmodule(submoduleConfig)
}
if file.HasInlineMergeConflicts {
return gui.switchToMerge()
}
if file.HasMergeConflicts {
return gui.createErrorPanel(gui.Tr.FileStagingRequirements)
}
return gui.pushContext(gui.State.Contexts.Staging, opts)
}
func (gui *Gui) handleFilePress() error {
node := gui.getSelectedFileNode()
if node == nil {
return nil
}
if node.IsLeaf() {
file := node.File
if file.HasInlineMergeConflicts {
return gui.switchToMerge()
}
if file.HasUnstagedChanges {
gui.logAction(gui.Tr.Actions.StageFile)
if err := gui.Git.WorkingTree.StageFile(file.Name); err != nil {
return gui.surfaceError(err)
}
} else {
gui.logAction(gui.Tr.Actions.UnstageFile)
if err := gui.Git.WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil {
return gui.surfaceError(err)
}
}
} else {
// if any files within have inline merge conflicts we can't stage or unstage,
// or it'll end up with those >>>>>> lines actually staged
if node.GetHasInlineMergeConflicts() {
return gui.createErrorPanel(gui.Tr.ErrStageDirWithInlineMergeConflicts)
}
if node.GetHasUnstagedChanges() {
gui.logAction(gui.Tr.Actions.StageFile)
if err := gui.Git.WorkingTree.StageFile(node.Path); err != nil {
return gui.surfaceError(err)
}
} else {
// pretty sure it doesn't matter that we're always passing true here
gui.logAction(gui.Tr.Actions.UnstageFile)
if err := gui.Git.WorkingTree.UnStageFile([]string{node.Path}, true); err != nil {
return gui.surfaceError(err)
}
}
}
if err := gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}); err != nil {
return err
}
return gui.State.Contexts.Files.HandleFocus()
}
func (gui *Gui) allFilesStaged() bool {
for _, file := range gui.State.FileManager.GetAllFiles() {
if file.HasUnstagedChanges {
return false
}
}
return true
}
func (gui *Gui) onFocusFile() error {
gui.takeOverMergeConflictScrolling()
return nil
}
func (gui *Gui) handleStageAll() error {
var err error
if gui.allFilesStaged() {
gui.logAction(gui.Tr.Actions.UnstageAllFiles)
err = gui.Git.WorkingTree.UnstageAll()
} else {
gui.logAction(gui.Tr.Actions.StageAllFiles)
err = gui.Git.WorkingTree.StageAll()
}
if err != nil {
_ = gui.surfaceError(err)
}
if err := gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}); err != nil {
return err
}
return gui.State.Contexts.Files.HandleFocus()
}
func (gui *Gui) handleIgnoreFile() error {
node := gui.getSelectedFileNode()
if node == nil {
return nil
}
if node.GetPath() == ".gitignore" {
return gui.createErrorPanel("Cannot ignore .gitignore")
}
unstageFiles := func() error {
return node.ForEachFile(func(file *models.File) error {
if file.HasStagedChanges {
if err := gui.Git.WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil {
return err
}
}
return nil
})
}

View file

@ -0,0 +1,298 @@
package gui
import (
"fmt"
"regexp"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/nodetree"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// list panel functions
func (gui *Gui) getSelectednodeNode() *nodetree.nodeNode {
selectedLine := gui.State.Panels.nodes.SelectedLineIdx
if selectedLine == -1 {
return nil
}
return gui.State.nodeManager.GetItemAtIndex(selectedLine)
}
func (gui *Gui) getSelectednode() *models.node {
node := gui.getSelectednodeNode()
if node == nil {
return nil
}
return node.node
}
func (gui *Gui) getSelectedPath() string {
node := gui.getSelectednodeNode()
if node == nil {
return ""
}
return node.GetPath()
}
func (gui *Gui) nodesRenderToMain() error {
node := gui.getSelectednodeNode()
if node == nil {
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: "",
task: NewRenderStringTask(gui.Tr.NoChangednodes),
},
})
}
if node.node != nil && node.File.HasInlineMergeConflicts {
return gui.renderConflictsFromFilesPanel()
}
cmdObj := gui.Git.WorkingTree.WorktreeFileDiffCmdObj(node, false, !node.GetHasUnstagedChanges() && node.GetHasStagedChanges(), gui.State.IgnoreWhitespaceInDiffView)
refreshOpts := refreshMainOpts{main: &viewUpdateOpts{
title: gui.Tr.UnstagedChanges,
task: NewRunPtyTask(cmdObj.GetCmd()),
}}
if node.GetHasUnstagedChanges() {
if node.GetHasStagedChanges() {
cmdObj := gui.Git.WorkingTree.WorktreeFileDiffCmdObj(node, false, true, gui.State.IgnoreWhitespaceInDiffView)
refreshOpts.secondary = &viewUpdateOpts{
title: gui.Tr.StagedChanges,
task: NewRunPtyTask(cmdObj.GetCmd()),
}
}
} else {
refreshOpts.main.title = gui.Tr.StagedChanges
}
return gui.refreshMainViews(refreshOpts)
}
func (gui *Gui) refreshFilesAndSubmodules() error {
gui.Mutexes.RefreshingFilesMutex.Lock()
gui.State.IsRefreshingFiles = true
defer func() {
gui.State.IsRefreshingFiles = false
gui.Mutexes.RefreshingFilesMutex.Unlock()
}()
selectedPath := gui.getSelectedPath()
if err := gui.refreshStateSubmoduleConfigs(); err != nil {
return err
}
if err := gui.refreshStateFiles(); err != nil {
return err
}
gui.OnUIThread(func() error {
if err := gui.postRefreshUpdate(gui.State.Contexts.Submodules); err != nil {
gui.Log.Error(err)
}
if ContextKey(gui.Views.Files.Context) == FILES_CONTEXT_KEY {
// doing this a little custom (as opposed to using gui.postRefreshUpdate) because we handle selecting the file explicitly below
if err := gui.State.Contexts.Files.HandleRender(); err != nil {
return err
}
}
if gui.currentContext().GetKey() == FILES_CONTEXT_KEY || (gui.g.CurrentView() == gui.Views.Main && ContextKey(gui.g.CurrentView().Context) == MAIN_MERGING_CONTEXT_KEY) {
newSelectedPath := gui.getSelectedPath()
alreadySelected := selectedPath != "" && newSelectedPath == selectedPath
if !alreadySelected {
gui.takeOverMergeConflictScrolling()
}
gui.Views.Files.FocusPoint(0, gui.State.Panels.Files.SelectedLineIdx)
return gui.filesRenderToMain()
}
return nil
})
return nil
}
// specific functions
func (gui *Gui) stagedFiles() []*models.File {
files := gui.State.FileManager.GetAllFiles()
result := make([]*models.File, 0)
for _, file := range files {
if file.HasStagedChanges {
result = append(result, file)
}
}
return result
}
func (gui *Gui) trackedFiles() []*models.File {
files := gui.State.FileManager.GetAllFiles()
result := make([]*models.File, 0, len(files))
for _, file := range files {
if file.Tracked {
result = append(result, file)
}
}
return result
}
func (gui *Gui) stageSelectedFile() error {
file := gui.getSelectedFile()
if file == nil {
return nil
}
return gui.Git.WorkingTree.StageFile(file.Name)
}
func (gui *Gui) handleEnterFile() error {
return gui.enterFile(OnFocusOpts{ClickedViewName: "", ClickedViewLineIdx: -1})
}
func (gui *Gui) enterFile(opts OnFocusOpts) error {
node := gui.getSelectedFileNode()
if node == nil {
return nil
}
if node.File == nil {
return gui.handleToggleDirCollapsed()
}
file := node.File
submoduleConfigs := gui.State.Submodules
if file.IsSubmodule(submoduleConfigs) {
submoduleConfig := file.SubmoduleConfig(submoduleConfigs)
return gui.enterSubmodule(submoduleConfig)
}
if file.HasInlineMergeConflicts {
return gui.switchToMerge()
}
if file.HasMergeConflicts {
return gui.createErrorPanel(gui.Tr.FileStagingRequirements)
}
return gui.pushContext(gui.State.Contexts.Staging, opts)
}
func (gui *Gui) handleFilePress() error {
node := gui.getSelectedFileNode()
if node == nil {
return nil
}
if node.IsLeaf() {
file := node.File
if file.HasInlineMergeConflicts {
return gui.switchToMerge()
}
if node.HasUnstagedChanges {
gui.logAction(gui.Tr.Actions.Stagenode)
if err := gui.Git.WorkingTree.Stagenode(node.Name); err != nil {
return gui.surfaceError(err)
}
} else {
gui.logAction(gui.Tr.Actions.Unstagenode)
if err := gui.Git.WorkingTree.UnStagenode(node.Names(), node.Tracked); err != nil {
return gui.surfaceError(err)
}
}
} else {
if node.GetHasInlineMergeConflicts() {
return gui.createErrorPanel(gui.Tr.ErrStageDirWithInlineMergeConflicts)
}
if node.GetHasUnstagedChanges() {
gui.logAction(gui.Tr.Actions.Stagenode)
if err := gui.Git.WorkingTree.Stagenode(node.Path); err != nil {
return gui.surfaceError(err)
}
} else {
// pretty sure it doesn't matter that we're always passing true here
gui.logAction(gui.Tr.Actions.Unstagenode)
if err := gui.Git.WorkingTree.UnStagenode([]string{node.Path}, true); err != nil {
return gui.surfaceError(err)
}
}
}
if err := gui.blah(refreshOptions{scope: []RefreshableView{nodeS}}); err != nil {
return err
}
return gui.State.Contexts.nodes.HandleFocus()
}
func (gui *Gui) allnodesStaged() bool {
for _, node := range gui.State.nodeManager.GetAllnodes() {
if node.HasUnstagedChanges {
return false
}
}
return true
}
func (gui *Gui) onFocusnode() error {
gui.takeOverMergeConflictScrolling()
return nil
}
func (gui *Gui) handleStageAll() error {
var err error
if gui.allnodesStaged() {
gui.logAction(gui.Tr.Actions.UnstageAllnodes)
err = gui.Git.WorkingTree.UnstageAll()
} else {
gui.logAction(gui.Tr.Actions.StageAllnodes)
err = gui.Git.WorkingTree.StageAll()
}
if err != nil {
_ = gui.surfaceError(err)
}
if err := gui.blah(refreshOptions{scope: []RefreshableView{nodeS}}); err != nil {
return err
}
return gui.State.Contexts.nodes.HandleFocus()
}
func (gui *Gui) handleIgnorenode() error {
node := gui.getSelectednodeNode()
if node == nil {
return nil
}
if node.GetPath() == ".gitignore" {
return gui.createErrorPanel("Cannot ignore .gitignore")
}
unstagenodes := func() error {
return node.ForEachnode(func(node *models.node) error {
if node.HasStagedChanges {
if err := gui.Git.WorkingTree.UnStagenode(node.Names(), node.Tracked); err != nil {
return err
}
}
return nil
})
}

View file

@ -0,0 +1,33 @@
type createMenuOptions struct {
showCancel bool
}
func (gui *Gui) createMenu(title string, items []*menuItem, createMenuOptions createMenuOptions) error {
if createMenuOptions.showCancel {
// this is mutative but I'm okay with that for now
items = append(items, &menuItem{
displayStrings: []string{gui.Tr.LcCancel},
onPress: func() error {
return nil
},
})
}
gui.State.MenuItems = items
stringArrays := make([][]string, len(items))
for i, item := range items {
if item.opensMenu && item.displayStrings != nil {
return errors.New("Message for the developer of this app: you've set opensMenu with displaystrings on the menu panel. Bad developer!. Apologies, user")
}
if item.displayStrings == nil {
styledStr := item.displayString
if item.opensMenu {
styledStr = opensMenuStyle(styledStr)
}
stringArrays[i] = []string{styledStr}
} else {
stringArrays[i] = item.displayStrings
}
}

View file

@ -0,0 +1,33 @@
type createMenuOptions struct {
showCancel bool
}
func (gui *Gui) createMenu(title string, items []*menuItem, createMenuOptions createMenuOptions) error {
if createMenuOptions.showCancel {
// this is mutative but I'm okay with that for now
items = app(items, &menuItem{
d: []string{gui.Tr.LcCancel},
onPress: func() error {
return nil
},
})
}
gui.State.MenuItems = items
stringArrays := make([][]string, len(items))
for i, items := range items {
if items.opensMenu && item.displayStrings != nil {
return errors.New("Message for the developer of this app: you've set opensMenu with displaystrings on the menu panel. Bad developer!. Apologies, user")
}
if item.displayStrings == nil {
styledStr := item.displayString
if item.opensMenu {
styledStr = opensMenuStyle(styledStr)
}
stringArrays[0] = []str0ng{styledStr}
} else {
str0ngArrays[0] = item.displayStrings
}
}

View file

@ -0,0 +1 @@
{"KeyEvents":[{"Timestamp":671,"Mod":0,"Key":13,"Ch":13},{"Timestamp":1095,"Mod":0,"Key":256,"Ch":32},{"Timestamp":1447,"Mod":0,"Key":256,"Ch":32},{"Timestamp":2608,"Mod":0,"Key":258,"Ch":0},{"Timestamp":2743,"Mod":0,"Key":258,"Ch":0},{"Timestamp":2973,"Mod":0,"Key":256,"Ch":118},{"Timestamp":3078,"Mod":0,"Key":258,"Ch":0},{"Timestamp":3215,"Mod":0,"Key":258,"Ch":0},{"Timestamp":3415,"Mod":0,"Key":256,"Ch":32},{"Timestamp":3920,"Mod":0,"Key":9,"Ch":9},{"Timestamp":4287,"Mod":0,"Key":257,"Ch":0},{"Timestamp":4431,"Mod":0,"Key":257,"Ch":0},{"Timestamp":4559,"Mod":0,"Key":257,"Ch":0},{"Timestamp":4848,"Mod":0,"Key":256,"Ch":32},{"Timestamp":5774,"Mod":0,"Key":9,"Ch":9},{"Timestamp":6031,"Mod":0,"Key":258,"Ch":0},{"Timestamp":6294,"Mod":0,"Key":257,"Ch":0},{"Timestamp":6374,"Mod":0,"Key":256,"Ch":118},{"Timestamp":6463,"Mod":0,"Key":258,"Ch":0},{"Timestamp":6591,"Mod":0,"Key":258,"Ch":0},{"Timestamp":6711,"Mod":0,"Key":256,"Ch":32},{"Timestamp":7274,"Mod":0,"Key":27,"Ch":0},{"Timestamp":7591,"Mod":0,"Key":258,"Ch":0},{"Timestamp":7968,"Mod":0,"Key":13,"Ch":13},{"Timestamp":8735,"Mod":0,"Key":256,"Ch":97},{"Timestamp":9039,"Mod":0,"Key":256,"Ch":32},{"Timestamp":9327,"Mod":0,"Key":258,"Ch":0},{"Timestamp":9478,"Mod":0,"Key":258,"Ch":0},{"Timestamp":9815,"Mod":0,"Key":256,"Ch":32},{"Timestamp":10439,"Mod":0,"Key":9,"Ch":9},{"Timestamp":11383,"Mod":0,"Key":256,"Ch":97},{"Timestamp":12095,"Mod":0,"Key":256,"Ch":97},{"Timestamp":12319,"Mod":0,"Key":257,"Ch":0},{"Timestamp":13039,"Mod":0,"Key":256,"Ch":32},{"Timestamp":14109,"Mod":0,"Key":27,"Ch":0},{"Timestamp":15119,"Mod":0,"Key":13,"Ch":13},{"Timestamp":15543,"Mod":0,"Key":256,"Ch":100},{"Timestamp":15855,"Mod":0,"Key":13,"Ch":13},{"Timestamp":16183,"Mod":0,"Key":256,"Ch":100},{"Timestamp":16415,"Mod":0,"Key":13,"Ch":13},{"Timestamp":16832,"Mod":0,"Key":258,"Ch":0},{"Timestamp":17150,"Mod":0,"Key":258,"Ch":0},{"Timestamp":17519,"Mod":0,"Key":256,"Ch":118},{"Timestamp":17654,"Mod":0,"Key":258,"Ch":0},{"Timestamp":17784,"Mod":0,"Key":258,"Ch":0},{"Timestamp":17903,"Mod":0,"Key":258,"Ch":0},{"Timestamp":18015,"Mod":0,"Key":258,"Ch":0},{"Timestamp":18150,"Mod":0,"Key":258,"Ch":0},{"Timestamp":18272,"Mod":0,"Key":258,"Ch":0},{"Timestamp":18567,"Mod":0,"Key":256,"Ch":100},{"Timestamp":18759,"Mod":0,"Key":13,"Ch":13},{"Timestamp":19254,"Mod":0,"Key":258,"Ch":0},{"Timestamp":19736,"Mod":0,"Key":259,"Ch":0},{"Timestamp":20358,"Mod":0,"Key":256,"Ch":100},{"Timestamp":20552,"Mod":0,"Key":13,"Ch":13},{"Timestamp":20871,"Mod":0,"Key":256,"Ch":100},{"Timestamp":20991,"Mod":0,"Key":13,"Ch":13},{"Timestamp":21433,"Mod":0,"Key":27,"Ch":0},{"Timestamp":21647,"Mod":0,"Key":258,"Ch":0},{"Timestamp":21943,"Mod":0,"Key":13,"Ch":13},{"Timestamp":22663,"Mod":0,"Key":256,"Ch":97},{"Timestamp":23207,"Mod":0,"Key":258,"Ch":0},{"Timestamp":23383,"Mod":0,"Key":258,"Ch":0},{"Timestamp":24039,"Mod":0,"Key":256,"Ch":100},{"Timestamp":24391,"Mod":0,"Key":13,"Ch":13},{"Timestamp":25141,"Mod":0,"Key":27,"Ch":0},{"Timestamp":25695,"Mod":0,"Key":256,"Ch":99},{"Timestamp":25959,"Mod":0,"Key":256,"Ch":116},{"Timestamp":26007,"Mod":0,"Key":256,"Ch":101},{"Timestamp":26191,"Mod":0,"Key":256,"Ch":115},{"Timestamp":26214,"Mod":0,"Key":256,"Ch":116},{"Timestamp":26464,"Mod":0,"Key":13,"Ch":13},{"Timestamp":27367,"Mod":0,"Key":256,"Ch":113}],"ResizeEvents":[{"Timestamp":0,"Width":272,"Height":74}]}

View file

@ -0,0 +1,18 @@
#!/bin/sh
cd $1
git init
git config user.email "CI@example.com"
git config user.name "CI"
cp ../files/one.txt one.txt
cp ../files/two.txt two.txt
cp ../files/three.txt three.txt
git add .
git commit -am file1
cp ../files/one_new.txt one.txt
cp ../files/two_new.txt two.txt
cp ../files/three_new.txt three.txt

View file

@ -0,0 +1,4 @@
{
"description": "Staging a file line-by-line",
"speed": 20
}