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/commands/types/enums" "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/gui/filetree" "github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts" "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.FileTreeViewModel.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 { ok, err := gui.setConflictsAndRenderWithLock(node.GetPath(), false) if err != nil { return err } if ok { return nil } } gui.resetMergeStateWithLock() 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() }() prevSelectedPath := gui.getSelectedPath() if err := gui.refreshStateSubmoduleConfigs(); err != nil { return err } if err := gui.refreshMergeState(); 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 { currentSelectedPath := gui.getSelectedPath() alreadySelected := prevSelectedPath != "" && currentSelectedPath == prevSelectedPath 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.FileTreeViewModel.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.FileTreeViewModel.GetAllFiles() result := make([]*models.File, 0, len(files)) for _, file := range files { if file.Tracked { result = append(result, file) } } return result } 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.FileTreeViewModel.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 }) } if node.GetIsTracked() { return gui.ask(askOpts{ title: gui.Tr.IgnoreTracked, prompt: gui.Tr.IgnoreTrackedPrompt, handleConfirm: func() error { gui.logAction(gui.Tr.Actions.IgnoreFile) // not 100% sure if this is necessary but I'll assume it is if err := unstageFiles(); err != nil { return err } if err := gui.Git.WorkingTree.RemoveTrackedFiles(node.GetPath()); err != nil { return err } if err := gui.Git.WorkingTree.Ignore(node.GetPath()); err != nil { return err } return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}) }, }) } gui.logAction(gui.Tr.Actions.IgnoreFile) if err := unstageFiles(); err != nil { return err } if err := gui.Git.WorkingTree.Ignore(node.GetPath()); err != nil { return gui.surfaceError(err) } return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}) } func (gui *Gui) handleWIPCommitPress() error { skipHookPrefix := gui.UserConfig.Git.SkipHookPrefix if skipHookPrefix == "" { return gui.createErrorPanel(gui.Tr.SkipHookPrefixNotConfigured) } textArea := gui.Views.CommitMessage.TextArea textArea.Clear() textArea.TypeString(skipHookPrefix) gui.Views.CommitMessage.RenderTextArea() return gui.handleCommitPress() } func (gui *Gui) commitPrefixConfigForRepo() *config.CommitPrefixConfig { cfg, ok := gui.UserConfig.Git.CommitPrefixes[utils.GetCurrentRepoName()] if !ok { return nil } return &cfg } func (gui *Gui) prepareFilesForCommit() error { noStagedFiles := len(gui.stagedFiles()) == 0 if noStagedFiles && gui.UserConfig.Gui.SkipNoStagedFilesWarning { gui.logAction(gui.Tr.Actions.StageAllFiles) err := gui.Git.WorkingTree.StageAll() if err != nil { return err } return gui.refreshFilesAndSubmodules() } return nil } func (gui *Gui) handleCommitPress() error { if err := gui.prepareFilesForCommit(); err != nil { return gui.surfaceError(err) } if gui.State.FileTreeViewModel.GetItemsLength() == 0 { return gui.createErrorPanel(gui.Tr.NoFilesStagedTitle) } if len(gui.stagedFiles()) == 0 { return gui.promptToStageAllAndRetry(gui.handleCommitPress) } if len(gui.State.failedCommitMessage) > 0 { gui.Views.CommitMessage.ClearTextArea() gui.Views.CommitMessage.TextArea.TypeString(gui.State.failedCommitMessage) gui.Views.CommitMessage.RenderTextArea() } else { commitPrefixConfig := gui.commitPrefixConfigForRepo() if commitPrefixConfig != nil { prefixPattern := commitPrefixConfig.Pattern prefixReplace := commitPrefixConfig.Replace rgx, err := regexp.Compile(prefixPattern) if err != nil { return gui.createErrorPanel(fmt.Sprintf("%s: %s", gui.Tr.LcCommitPrefixPatternError, err.Error())) } prefix := rgx.ReplaceAllString(gui.getCheckedOutBranch().Name, prefixReplace) gui.Views.CommitMessage.ClearTextArea() gui.Views.CommitMessage.TextArea.TypeString(prefix) gui.Views.CommitMessage.RenderTextArea() } } if err := gui.pushContext(gui.State.Contexts.CommitMessage); err != nil { return err } gui.RenderCommitLength() return nil } func (gui *Gui) promptToStageAllAndRetry(retry func() error) error { return gui.ask(askOpts{ title: gui.Tr.NoFilesStagedTitle, prompt: gui.Tr.NoFilesStagedPrompt, handleConfirm: func() error { gui.logAction(gui.Tr.Actions.StageAllFiles) if err := gui.Git.WorkingTree.StageAll(); err != nil { return gui.surfaceError(err) } if err := gui.refreshFilesAndSubmodules(); err != nil { return gui.surfaceError(err) } return retry() }, }) } func (gui *Gui) handleAmendCommitPress() error { if gui.State.FileTreeViewModel.GetItemsLength() == 0 { return gui.createErrorPanel(gui.Tr.NoFilesStagedTitle) } if len(gui.stagedFiles()) == 0 { return gui.promptToStageAllAndRetry(gui.handleAmendCommitPress) } if len(gui.State.Commits) == 0 { return gui.createErrorPanel(gui.Tr.NoCommitToAmend) } return gui.ask(askOpts{ title: strings.Title(gui.Tr.AmendLastCommit), prompt: gui.Tr.SureToAmend, handleConfirm: func() error { cmdObj := gui.Git.Commit.AmendHeadCmdObj() gui.logAction(gui.Tr.Actions.AmendCommit) return gui.withGpgHandling(cmdObj, gui.Tr.AmendingStatus, nil) }, }) } // handleCommitEditorPress - handle when the user wants to commit changes via // their editor rather than via the popup panel func (gui *Gui) handleCommitEditorPress() error { if gui.State.FileTreeViewModel.GetItemsLength() == 0 { return gui.createErrorPanel(gui.Tr.NoFilesStagedTitle) } if len(gui.stagedFiles()) == 0 { return gui.promptToStageAllAndRetry(gui.handleCommitEditorPress) } gui.logAction(gui.Tr.Actions.Commit) return gui.runSubprocessWithSuspenseAndRefresh( gui.Git.Commit.CommitEditorCmdObj(), ) } func (gui *Gui) handleStatusFilterPressed() error { menuItems := []*menuItem{ { displayString: gui.Tr.FilterStagedFiles, onPress: func() error { return gui.setStatusFiltering(filetree.DisplayStaged) }, }, { displayString: gui.Tr.FilterUnstagedFiles, onPress: func() error { return gui.setStatusFiltering(filetree.DisplayUnstaged) }, }, { displayString: gui.Tr.ResetCommitFilterState, onPress: func() error { return gui.setStatusFiltering(filetree.DisplayAll) }, }, } return gui.createMenu(gui.Tr.FilteringMenuTitle, menuItems, createMenuOptions{showCancel: true}) } func (gui *Gui) setStatusFiltering(filter filetree.FileTreeDisplayFilter) error { state := gui.State state.FileTreeViewModel.SetFilter(filter) return gui.handleRefreshFiles() } func (gui *Gui) editFile(filename string) error { return gui.editFileAtLine(filename, 1) } func (gui *Gui) editFileAtLine(filename string, lineNumber int) error { cmdStr, err := gui.Git.File.GetEditCmdStr(filename, lineNumber) if err != nil { return gui.surfaceError(err) } gui.logAction(gui.Tr.Actions.EditFile) return gui.runSubprocessWithSuspenseAndRefresh( gui.OSCommand.Cmd.NewShell(cmdStr), ) } func (gui *Gui) handleFileEdit() error { node := gui.getSelectedFileNode() if node == nil { return nil } if node.File == nil { return gui.createErrorPanel(gui.Tr.ErrCannotEditDirectory) } return gui.editFile(node.GetPath()) } func (gui *Gui) handleFileOpen() error { node := gui.getSelectedFileNode() if node == nil { return nil } return gui.openFile(node.GetPath()) } func (gui *Gui) handleRefreshFiles() error { return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}) } func (gui *Gui) refreshStateFiles() error { state := gui.State // keep track of where the cursor is currently and the current file names // when we refresh, go looking for a matching name // move the cursor to there. selectedNode := gui.getSelectedFileNode() prevNodes := gui.State.FileTreeViewModel.GetAllItems() prevSelectedLineIdx := gui.State.Panels.Files.SelectedLineIdx // If git thinks any of our files have inline merge conflicts, but they actually don't, // we stage them. // Note that if files with merge conflicts have both arisen and have been resolved // between refreshes, we won't stage them here. This is super unlikely though, // and this approach spares us from having to call `git status` twice in a row. // Although this also means that at startup we won't be staging anything until // we call git status again. pathsToStage := []string{} prevConflictFileCount := 0 for _, file := range state.FileTreeViewModel.GetAllFiles() { if file.HasMergeConflicts { prevConflictFileCount++ } if file.HasInlineMergeConflicts { hasConflicts, err := mergeconflicts.FileHasConflictMarkers(file.Name) if err != nil { gui.Log.Error(err) } else if !hasConflicts { pathsToStage = append(pathsToStage, file.Name) } } } if len(pathsToStage) > 0 { gui.logAction(gui.Tr.Actions.StageResolvedFiles) if err := gui.Git.WorkingTree.StageFiles(pathsToStage); err != nil { return gui.surfaceError(err) } } files := gui.Git.Loaders.Files. GetStatusFiles(loaders.GetStatusFileOptions{}) conflictFileCount := 0 for _, file := range files { if file.HasMergeConflicts { conflictFileCount++ } } if gui.Git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE && conflictFileCount == 0 && prevConflictFileCount > 0 { gui.OnUIThread(func() error { return gui.promptToContinueRebase() }) } // for when you stage the old file of a rename and the new file is in a collapsed dir state.FileTreeViewModel.RWMutex.Lock() for _, file := range files { if selectedNode != nil && selectedNode.Path != "" && file.PreviousName == selectedNode.Path { state.FileTreeViewModel.ExpandToPath(file.Name) } } // only taking over the filter if it hasn't already been set by the user. // Though this does make it impossible for the user to actually say they want to display all if // conflicts are currently being shown. Hmm. Worth it I reckon. If we need to add some // extra state here to see if the user's set the filter themselves we can do that, but // I'd prefer to maintain as little state as possible. if conflictFileCount > 0 { if state.FileTreeViewModel.GetFilter() == filetree.DisplayAll { state.FileTreeViewModel.SetFilter(filetree.DisplayConflicted) } } else if state.FileTreeViewModel.GetFilter() == filetree.DisplayConflicted { state.FileTreeViewModel.SetFilter(filetree.DisplayAll) } state.FileTreeViewModel.SetFiles(files) state.FileTreeViewModel.RWMutex.Unlock() if err := gui.fileWatcher.addFilesToFileWatcher(files); err != nil { return err } if selectedNode != nil { newIdx := gui.findNewSelectedIdx(prevNodes[prevSelectedLineIdx:], state.FileTreeViewModel.GetAllItems()) if newIdx != -1 && newIdx != prevSelectedLineIdx { newNode := state.FileTreeViewModel.GetItemAtIndex(newIdx) // when not in tree mode, we show merge conflict files at the top, so you // can work through them one by one without having to sift through a large // set of files. If you have just fixed the merge conflicts of a file, we // actually don't want to jump to that file's new position, because that // file will now be ages away amidst the other files without merge // conflicts: the user in this case would rather work on the next file // with merge conflicts, which will have moved up to fill the gap left by // the last file, meaning the cursor doesn't need to move at all. leaveCursor := !state.FileTreeViewModel.InTreeMode() && newNode != nil && selectedNode.File != nil && selectedNode.File.HasMergeConflicts && newNode.File != nil && !newNode.File.HasMergeConflicts if !leaveCursor { state.Panels.Files.SelectedLineIdx = newIdx } } } gui.refreshSelectedLine(state.Panels.Files, state.FileTreeViewModel.GetItemsLength()) return nil } // promptToContinueRebase asks the user if they want to continue the rebase/merge that's in progress func (gui *Gui) promptToContinueRebase() error { gui.takeOverMergeConflictScrolling() return gui.ask(askOpts{ title: "continue", prompt: gui.Tr.ConflictsResolved, handleConfirm: func() error { return gui.genericMergeCommand(REBASE_OPTION_CONTINUE) }, }) } // Let's try to find our file again and move the cursor to that. // If we can't find our file, it was probably just removed by the user. In that // case, we go looking for where the next file has been moved to. Given that the // user could have removed a whole directory, we continue iterating through the old // nodes until we find one that exists in the new set of nodes, then move the cursor // to that. // prevNodes starts from our previously selected node because we don't need to consider anything above that func (gui *Gui) findNewSelectedIdx(prevNodes []*filetree.FileNode, currNodes []*filetree.FileNode) int { getPaths := func(node *filetree.FileNode) []string { if node == nil { return nil } if node.File != nil && node.File.IsRename() { return node.File.Names() } else { return []string{node.Path} } } for _, prevNode := range prevNodes { selectedPaths := getPaths(prevNode) for idx, node := range currNodes { paths := getPaths(node) // If you started off with a rename selected, and now it's broken in two, we want you to jump to the new file, not the old file. // This is because the new should be in the same position as the rename was meaning less cursor jumping foundOldFileInRename := prevNode.File != nil && prevNode.File.IsRename() && node.Path == prevNode.File.PreviousName foundNode := utils.StringArraysOverlap(paths, selectedPaths) && !foundOldFileInRename if foundNode { return idx } } } return -1 } func (gui *Gui) handlePullFiles() error { if gui.popupPanelFocused() { return nil } action := gui.Tr.Actions.Pull currentBranch := gui.currentBranch() if currentBranch == nil { // need to wait for branches to refresh return nil } // if we have no upstream branch we need to set that first if !currentBranch.IsTrackingRemote() { suggestedRemote := getSuggestedRemote(gui.State.Remotes) return gui.prompt(promptOpts{ title: gui.Tr.EnterUpstream, initialContent: suggestedRemote + " " + currentBranch.Name, findSuggestionsFunc: gui.getRemoteBranchesSuggestionsFunc(" "), handleConfirm: func(upstream string) error { var upstreamBranch, upstreamRemote string split := strings.Split(upstream, " ") if len(split) != 2 { return gui.createErrorPanel(gui.Tr.InvalidUpstream) } upstreamRemote = split[0] upstreamBranch = split[1] if err := gui.Git.Branch.SetCurrentBranchUpstream(upstreamRemote, upstreamBranch); err != nil { errorMessage := err.Error() if strings.Contains(errorMessage, "does not exist") { errorMessage = fmt.Sprintf("upstream branch %s not found.\nIf you expect it to exist, you should fetch (with 'f').\nOtherwise, you should push (with 'shift+P')", upstream) } return gui.createErrorPanel(errorMessage) } return gui.pullFiles(PullFilesOptions{UpstreamRemote: upstreamRemote, UpstreamBranch: upstreamBranch, action: action}) }, }) } return gui.pullFiles(PullFilesOptions{UpstreamRemote: currentBranch.UpstreamRemote, UpstreamBranch: currentBranch.UpstreamBranch, action: action}) } type PullFilesOptions struct { UpstreamRemote string UpstreamBranch string FastForwardOnly bool action string } func (gui *Gui) pullFiles(opts PullFilesOptions) error { if err := gui.createLoaderPanel(gui.Tr.PullWait); err != nil { return err } // TODO: this doesn't look like a good idea. Why the goroutine? go utils.Safe(func() { _ = gui.pullWithLock(opts) }) return nil } func (gui *Gui) pullWithLock(opts PullFilesOptions) error { gui.Mutexes.FetchMutex.Lock() defer gui.Mutexes.FetchMutex.Unlock() gui.logAction(opts.action) err := gui.Git.Sync.Pull( git_commands.PullOptions{ RemoteName: opts.UpstreamRemote, BranchName: opts.UpstreamBranch, FastForwardOnly: opts.FastForwardOnly, }, ) if err == nil { _ = gui.closeConfirmationPrompt(false) } return gui.handleGenericMergeCommandResult(err) } type pushOpts struct { force bool upstreamRemote string upstreamBranch string setUpstream bool } func (gui *Gui) push(opts pushOpts) error { if err := gui.createLoaderPanel(gui.Tr.PushWait); err != nil { return err } go utils.Safe(func() { gui.logAction(gui.Tr.Actions.Push) err := gui.Git.Sync.Push(git_commands.PushOpts{ Force: opts.force, UpstreamRemote: opts.upstreamRemote, UpstreamBranch: opts.upstreamBranch, SetUpstream: opts.setUpstream, }) if err != nil && !opts.force && strings.Contains(err.Error(), "Updates were rejected") { forcePushDisabled := gui.UserConfig.Git.DisableForcePushing if forcePushDisabled { _ = gui.createErrorPanel(gui.Tr.UpdatesRejectedAndForcePushDisabled) return } _ = gui.ask(askOpts{ title: gui.Tr.ForcePush, prompt: gui.Tr.ForcePushPrompt, handleConfirm: func() error { newOpts := opts newOpts.force = true return gui.push(newOpts) }, }) return } gui.handleCredentialsPopup(err) _ = gui.refreshSidePanels(refreshOptions{mode: ASYNC}) }) return nil } func (gui *Gui) pushFiles() error { if gui.popupPanelFocused() { return nil } // if we have pullables we'll ask if the user wants to force push currentBranch := gui.currentBranch() if currentBranch == nil { // need to wait for branches to refresh return nil } if currentBranch.IsTrackingRemote() { opts := pushOpts{ force: false, upstreamRemote: currentBranch.UpstreamRemote, upstreamBranch: currentBranch.UpstreamBranch, } if currentBranch.HasCommitsToPull() { opts.force = true return gui.requestToForcePush(opts) } else { return gui.push(opts) } } else { suggestedRemote := getSuggestedRemote(gui.State.Remotes) if gui.Git.Config.GetPushToCurrent() { return gui.push(pushOpts{setUpstream: true}) } else { return gui.prompt(promptOpts{ title: gui.Tr.EnterUpstream, initialContent: suggestedRemote + " " + currentBranch.Name, findSuggestionsFunc: gui.getRemoteBranchesSuggestionsFunc(" "), handleConfirm: func(upstream string) error { var upstreamBranch, upstreamRemote string split := strings.Split(upstream, " ") if len(split) == 2 { upstreamRemote = split[0] upstreamBranch = split[1] } else { upstreamRemote = upstream upstreamBranch = "" } return gui.push(pushOpts{ force: false, upstreamRemote: upstreamRemote, upstreamBranch: upstreamBranch, setUpstream: true, }) }, }) } } } func getSuggestedRemote(remotes []*models.Remote) string { if len(remotes) == 0 { return "origin" } for _, remote := range remotes { if remote.Name == "origin" { return remote.Name } } return remotes[0].Name } func (gui *Gui) requestToForcePush(opts pushOpts) error { forcePushDisabled := gui.UserConfig.Git.DisableForcePushing if forcePushDisabled { return gui.createErrorPanel(gui.Tr.ForcePushDisabled) } return gui.ask(askOpts{ title: gui.Tr.ForcePush, prompt: gui.Tr.ForcePushPrompt, handleConfirm: func() error { return gui.push(opts) }, }) } func (gui *Gui) switchToMerge() error { file := gui.getSelectedFile() if file == nil { return nil } gui.takeOverMergeConflictScrolling() if gui.State.Panels.Merging.GetPath() != file.Name { hasConflicts, err := gui.setMergeStateWithLock(file.Name) if err != nil { return err } if !hasConflicts { return nil } } return gui.pushContext(gui.State.Contexts.Merging) } func (gui *Gui) openFile(filename string) error { gui.logAction(gui.Tr.Actions.OpenFile) if err := gui.OSCommand.OpenFile(filename); err != nil { return gui.surfaceError(err) } return nil } func (gui *Gui) handleCustomCommand() error { return gui.prompt(promptOpts{ title: gui.Tr.CustomCommand, findSuggestionsFunc: gui.getCustomCommandsHistorySuggestionsFunc(), handleConfirm: func(command string) error { gui.Config.GetAppState().CustomCommandsHistory = utils.Limit( utils.Uniq( append(gui.Config.GetAppState().CustomCommandsHistory, command), ), 1000, ) err := gui.Config.SaveAppState() if err != nil { gui.Log.Error(err) } gui.logAction(gui.Tr.Actions.CustomCommand) return gui.runSubprocessWithSuspenseAndRefresh( gui.OSCommand.Cmd.NewShell(command), ) }, }) } func (gui *Gui) handleCreateStashMenu() error { menuItems := []*menuItem{ { displayString: gui.Tr.LcStashAllChanges, onPress: func() error { gui.logAction(gui.Tr.Actions.StashAllChanges) return gui.handleStashSave(gui.Git.Stash.Save) }, }, { displayString: gui.Tr.LcStashStagedChanges, onPress: func() error { gui.logAction(gui.Tr.Actions.StashStagedChanges) return gui.handleStashSave(gui.Git.Stash.SaveStagedChanges) }, }, } return gui.createMenu(gui.Tr.LcStashOptions, menuItems, createMenuOptions{showCancel: true}) } func (gui *Gui) handleStashChanges() error { return gui.handleStashSave(gui.Git.Stash.Save) } func (gui *Gui) handleCreateResetToUpstreamMenu() error { return gui.createResetMenu("@{upstream}") } func (gui *Gui) handleToggleDirCollapsed() error { node := gui.getSelectedFileNode() if node == nil { return nil } gui.State.FileTreeViewModel.ToggleCollapsed(node.GetPath()) if err := gui.postRefreshUpdate(gui.State.Contexts.Files); err != nil { gui.Log.Error(err) } return nil } func (gui *Gui) handleToggleFileTreeView() error { // get path of currently selected file path := gui.getSelectedPath() gui.State.FileTreeViewModel.ToggleShowTree() // find that same node in the new format and move the cursor to it if path != "" { gui.State.FileTreeViewModel.ExpandToPath(path) index, found := gui.State.FileTreeViewModel.GetIndexForPath(path) if found { gui.filesListContext().GetPanelState().SetSelectedLineIdx(index) } } if ContextKey(gui.Views.Files.Context) == FILES_CONTEXT_KEY { if err := gui.State.Contexts.Files.HandleRender(); err != nil { return err } if err := gui.State.Contexts.Files.HandleFocus(); err != nil { return err } } return nil } func (gui *Gui) handleOpenMergeTool() error { return gui.ask(askOpts{ title: gui.Tr.MergeToolTitle, prompt: gui.Tr.MergeToolPrompt, handleConfirm: func() error { gui.logAction(gui.Tr.Actions.OpenMergeTool) return gui.runSubprocessWithSuspenseAndRefresh( gui.Git.WorkingTree.OpenMergeToolCmdObj(), ) }, }) }