diff --git a/test/integration/staging/expected/.git_keep/COMMIT_EDITMSG b/test/integration/staging/expected/.git_keep/COMMIT_EDITMSG new file mode 100644 index 000000000..9daeafb98 --- /dev/null +++ b/test/integration/staging/expected/.git_keep/COMMIT_EDITMSG @@ -0,0 +1 @@ +test diff --git a/test/integration/staging/expected/.git_keep/FETCH_HEAD b/test/integration/staging/expected/.git_keep/FETCH_HEAD new file mode 100644 index 000000000..e69de29bb diff --git a/test/integration/staging/expected/.git_keep/HEAD b/test/integration/staging/expected/.git_keep/HEAD new file mode 100644 index 000000000..cb089cd89 --- /dev/null +++ b/test/integration/staging/expected/.git_keep/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/test/integration/staging/expected/.git_keep/config b/test/integration/staging/expected/.git_keep/config new file mode 100644 index 000000000..8ae104545 --- /dev/null +++ b/test/integration/staging/expected/.git_keep/config @@ -0,0 +1,10 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true + ignorecase = true + precomposeunicode = true +[user] + email = CI@example.com + name = CI diff --git a/test/integration/staging/expected/.git_keep/description b/test/integration/staging/expected/.git_keep/description new file mode 100644 index 000000000..498b267a8 --- /dev/null +++ b/test/integration/staging/expected/.git_keep/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/test/integration/staging/expected/.git_keep/index b/test/integration/staging/expected/.git_keep/index new file mode 100644 index 000000000..cdf1e9ab6 Binary files /dev/null and b/test/integration/staging/expected/.git_keep/index differ diff --git a/test/integration/staging/expected/.git_keep/info/exclude b/test/integration/staging/expected/.git_keep/info/exclude new file mode 100644 index 000000000..8e9f2071f --- /dev/null +++ b/test/integration/staging/expected/.git_keep/info/exclude @@ -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 diff --git a/test/integration/staging/expected/.git_keep/logs/HEAD b/test/integration/staging/expected/.git_keep/logs/HEAD new file mode 100644 index 000000000..8ad145985 --- /dev/null +++ b/test/integration/staging/expected/.git_keep/logs/HEAD @@ -0,0 +1,2 @@ +0000000000000000000000000000000000000000 6806c569c7c063ec7ed3d16da3faa32a1622bb4b CI 1642402468 +1100 commit (initial): file1 +6806c569c7c063ec7ed3d16da3faa32a1622bb4b 4253d2597aec2a480a8e9054250e6b4aa5b76d9e CI 1642402495 +1100 commit: test diff --git a/test/integration/staging/expected/.git_keep/logs/refs/heads/master b/test/integration/staging/expected/.git_keep/logs/refs/heads/master new file mode 100644 index 000000000..8ad145985 --- /dev/null +++ b/test/integration/staging/expected/.git_keep/logs/refs/heads/master @@ -0,0 +1,2 @@ +0000000000000000000000000000000000000000 6806c569c7c063ec7ed3d16da3faa32a1622bb4b CI 1642402468 +1100 commit (initial): file1 +6806c569c7c063ec7ed3d16da3faa32a1622bb4b 4253d2597aec2a480a8e9054250e6b4aa5b76d9e CI 1642402495 +1100 commit: test diff --git a/test/integration/staging/expected/.git_keep/objects/05/9586b468b89bf98e3b62126f455ab15bea4a5f b/test/integration/staging/expected/.git_keep/objects/05/9586b468b89bf98e3b62126f455ab15bea4a5f new file mode 100644 index 000000000..08eb1a0d1 Binary files /dev/null and b/test/integration/staging/expected/.git_keep/objects/05/9586b468b89bf98e3b62126f455ab15bea4a5f differ diff --git a/test/integration/staging/expected/.git_keep/objects/12/c4186053ecd4056526743060a8fe87429b7306 b/test/integration/staging/expected/.git_keep/objects/12/c4186053ecd4056526743060a8fe87429b7306 new file mode 100644 index 000000000..65f9a5559 Binary files /dev/null and b/test/integration/staging/expected/.git_keep/objects/12/c4186053ecd4056526743060a8fe87429b7306 differ diff --git a/test/integration/staging/expected/.git_keep/objects/3e/95c983db9349a26b20fccbdaa933e805ff817e b/test/integration/staging/expected/.git_keep/objects/3e/95c983db9349a26b20fccbdaa933e805ff817e new file mode 100644 index 000000000..2da57d308 Binary files /dev/null and b/test/integration/staging/expected/.git_keep/objects/3e/95c983db9349a26b20fccbdaa933e805ff817e differ diff --git a/test/integration/staging/expected/.git_keep/objects/40/ce5b93f72e04cb876afaaf91398c2821260b95 b/test/integration/staging/expected/.git_keep/objects/40/ce5b93f72e04cb876afaaf91398c2821260b95 new file mode 100644 index 000000000..8c6fa3ec6 Binary files /dev/null and b/test/integration/staging/expected/.git_keep/objects/40/ce5b93f72e04cb876afaaf91398c2821260b95 differ diff --git a/test/integration/staging/expected/.git_keep/objects/42/53d2597aec2a480a8e9054250e6b4aa5b76d9e b/test/integration/staging/expected/.git_keep/objects/42/53d2597aec2a480a8e9054250e6b4aa5b76d9e new file mode 100644 index 000000000..ed016e977 --- /dev/null +++ b/test/integration/staging/expected/.git_keep/objects/42/53d2597aec2a480a8e9054250e6b4aa5b76d9e @@ -0,0 +1,2 @@ +xA +0@Q9Ed4"BW=L:AR#x|{em-d8]:̘H"%IYjN/Oj{v+=H"<#Ntavg='n+ \ No newline at end of file diff --git a/test/integration/staging/expected/.git_keep/objects/79/8369253f104fe8cdc91db6f7d3525be532218e b/test/integration/staging/expected/.git_keep/objects/79/8369253f104fe8cdc91db6f7d3525be532218e new file mode 100644 index 000000000..f38ecd0b6 Binary files /dev/null and b/test/integration/staging/expected/.git_keep/objects/79/8369253f104fe8cdc91db6f7d3525be532218e differ diff --git a/test/integration/staging/expected/.git_keep/objects/a0/425534134de68284a0a7250b83b0e6303f0ed7 b/test/integration/staging/expected/.git_keep/objects/a0/425534134de68284a0a7250b83b0e6303f0ed7 new file mode 100644 index 000000000..258d99746 Binary files /dev/null and b/test/integration/staging/expected/.git_keep/objects/a0/425534134de68284a0a7250b83b0e6303f0ed7 differ diff --git a/test/integration/staging/expected/.git_keep/objects/a4/7182dc057408b3c6b1749cb46db0e0c5fd626b b/test/integration/staging/expected/.git_keep/objects/a4/7182dc057408b3c6b1749cb46db0e0c5fd626b new file mode 100644 index 000000000..49bb3dd6b Binary files /dev/null and b/test/integration/staging/expected/.git_keep/objects/a4/7182dc057408b3c6b1749cb46db0e0c5fd626b differ diff --git a/test/integration/staging/expected/.git_keep/objects/a4/8a7caa799e7859b8f21d373e3f01b06002d42f b/test/integration/staging/expected/.git_keep/objects/a4/8a7caa799e7859b8f21d373e3f01b06002d42f new file mode 100644 index 000000000..428e84de6 Binary files /dev/null and b/test/integration/staging/expected/.git_keep/objects/a4/8a7caa799e7859b8f21d373e3f01b06002d42f differ diff --git a/test/integration/staging/expected/.git_keep/objects/b6/77e3e5777e122a22ebb001532c5017b199b0c0 b/test/integration/staging/expected/.git_keep/objects/b6/77e3e5777e122a22ebb001532c5017b199b0c0 new file mode 100644 index 000000000..a50f73636 --- /dev/null +++ b/test/integration/staging/expected/.git_keep/objects/b6/77e3e5777e122a22ebb001532c5017b199b0c0 @@ -0,0 +1,2 @@ +xMn0 ;)NN@L:m 'Dp["O')4EC3<|8J4F&\WBXh07|^r0BQ88XCA=Iu>#>7Y|\OEotD=FƓ&mHDVR+q%)\KǛ#;dHaOB6 +Q» 8h*%KM}Q2"HGkϟ[`:DBUuJO=+4<3Zpo~lFU<WJy߯FMdqn&E۞8|m/)x_ 87עJU (?"#o -΢@ \ No newline at end of file diff --git a/test/integration/staging/expected/.git_keep/objects/dc/02541428fdc15b30bd2174fcbcd43d388eab82 b/test/integration/staging/expected/.git_keep/objects/dc/02541428fdc15b30bd2174fcbcd43d388eab82 new file mode 100644 index 000000000..232769986 Binary files /dev/null and b/test/integration/staging/expected/.git_keep/objects/dc/02541428fdc15b30bd2174fcbcd43d388eab82 differ diff --git a/test/integration/staging/expected/.git_keep/objects/e8/aaa2f356eb341c693e239467fd200d0117b487 b/test/integration/staging/expected/.git_keep/objects/e8/aaa2f356eb341c693e239467fd200d0117b487 new file mode 100644 index 000000000..f444e5a1e --- /dev/null +++ b/test/integration/staging/expected/.git_keep/objects/e8/aaa2f356eb341c693e239467fd200d0117b487 @@ -0,0 +1 @@ +x==0 S/4ީ-*@vllі0gr0'ɓY )/.8I2Mpp_D;⇘F<= oaq6bV =1IV#:w+6#&8{/ _kv(WmJ]z)} ]dαk™Yx*81<ȩp ~TC;k>^%G g ffPˆKΰ%x1\hhgD Ljۈdϟ[`:H YkKWEKtQqmaCRn~?QnR kmtqνs+|"x_!fvؓVůe'ڟwA/Z4!oZZ- b \ No newline at end of file diff --git a/test/integration/staging/expected/.git_keep/objects/fb/e09a11933b44ea60b46bd0f3d44142cb6189a4 b/test/integration/staging/expected/.git_keep/objects/fb/e09a11933b44ea60b46bd0f3d44142cb6189a4 new file mode 100644 index 000000000..3d85419a6 Binary files /dev/null and b/test/integration/staging/expected/.git_keep/objects/fb/e09a11933b44ea60b46bd0f3d44142cb6189a4 differ diff --git a/test/integration/staging/expected/.git_keep/refs/heads/master b/test/integration/staging/expected/.git_keep/refs/heads/master new file mode 100644 index 000000000..c94180bef --- /dev/null +++ b/test/integration/staging/expected/.git_keep/refs/heads/master @@ -0,0 +1 @@ +4253d2597aec2a480a8e9054250e6b4aa5b76d9e diff --git a/test/integration/staging/expected/one.txt b/test/integration/staging/expected/one.txt new file mode 100644 index 000000000..3a54c9212 --- /dev/null +++ b/test/integration/staging/expected/one.txt @@ -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 diff --git a/test/integration/staging/expected/three.txt b/test/integration/staging/expected/three.txt new file mode 100644 index 000000000..2a6e8cb28 --- /dev/null +++ b/test/integration/staging/expected/three.txt @@ -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 + }) + } diff --git a/test/integration/staging/expected/two.txt b/test/integration/staging/expected/two.txt new file mode 100644 index 000000000..b4ebbd4f1 --- /dev/null +++ b/test/integration/staging/expected/two.txt @@ -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 + } + } diff --git a/test/integration/staging/files/one.txt b/test/integration/staging/files/one.txt new file mode 100644 index 000000000..e8aaa2f35 --- /dev/null +++ b/test/integration/staging/files/one.txt @@ -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 diff --git a/test/integration/staging/files/one_new.txt b/test/integration/staging/files/one_new.txt new file mode 100644 index 000000000..3a54c9212 --- /dev/null +++ b/test/integration/staging/files/one_new.txt @@ -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 diff --git a/test/integration/staging/files/three.txt b/test/integration/staging/files/three.txt new file mode 100644 index 000000000..a04255341 --- /dev/null +++ b/test/integration/staging/files/three.txt @@ -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 + }) + } diff --git a/test/integration/staging/files/three_new.txt b/test/integration/staging/files/three_new.txt new file mode 100644 index 000000000..500ce9c2b --- /dev/null +++ b/test/integration/staging/files/three_new.txt @@ -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 + }) + } diff --git a/test/integration/staging/files/two.txt b/test/integration/staging/files/two.txt new file mode 100644 index 000000000..a48a7caa7 --- /dev/null +++ b/test/integration/staging/files/two.txt @@ -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 + } + } diff --git a/test/integration/staging/files/two_new.txt b/test/integration/staging/files/two_new.txt new file mode 100644 index 000000000..bbe2a2f2c --- /dev/null +++ b/test/integration/staging/files/two_new.txt @@ -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 + } + } diff --git a/test/integration/staging/recording.json b/test/integration/staging/recording.json new file mode 100644 index 000000000..912fe35b0 --- /dev/null +++ b/test/integration/staging/recording.json @@ -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}]} \ No newline at end of file diff --git a/test/integration/staging/setup.sh b/test/integration/staging/setup.sh new file mode 100644 index 000000000..5ede99e27 --- /dev/null +++ b/test/integration/staging/setup.sh @@ -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 diff --git a/test/integration/staging/test.json b/test/integration/staging/test.json new file mode 100644 index 000000000..9cadc6b85 --- /dev/null +++ b/test/integration/staging/test.json @@ -0,0 +1,4 @@ +{ + "description": "Staging a file line-by-line", + "speed": 20 +}