1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2024-12-12 11:15:00 +02:00
lazygit/pkg/gui/files_panel.go
2022-01-27 21:25:04 +11:00

1066 lines
28 KiB
Go

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(),
)
},
})
}