diff options
-rw-r--r-- | pkg/commands/errors.go | 14 | ||||
-rw-r--r-- | pkg/commands/git.go | 77 | ||||
-rw-r--r-- | pkg/commands/os.go | 14 | ||||
-rw-r--r-- | pkg/gui/commit_files_panel.go | 43 | ||||
-rw-r--r-- | pkg/gui/commits_panel.go | 37 | ||||
-rw-r--r-- | pkg/gui/gui.go | 44 | ||||
-rw-r--r-- | pkg/gui/keybindings.go | 8 | ||||
-rw-r--r-- | pkg/gui/view_helpers.go | 52 | ||||
-rw-r--r-- | pkg/i18n/dutch.go | 9 | ||||
-rw-r--r-- | pkg/i18n/english.go | 9 | ||||
-rw-r--r-- | pkg/i18n/polish.go | 9 |
11 files changed, 221 insertions, 95 deletions
diff --git a/pkg/commands/errors.go b/pkg/commands/errors.go new file mode 100644 index 000000000..4723eb95d --- /dev/null +++ b/pkg/commands/errors.go @@ -0,0 +1,14 @@ +package commands + +import "github.com/go-errors/errors" + +// WrapError wraps an error for the sake of showing a stack trace at the top level +// the go-errors package, for some reason, does not return nil when you try to wrap +// a non-error, so we're just doing it here +func WrapError(err error) error { + if err == nil { + return err + } + + return errors.Wrap(err, 0) +} diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 96b25316c..c4de147cd 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -32,11 +32,11 @@ func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir f } if !os.IsNotExist(err) { - return errors.Wrap(err, 0) + return WrapError(err) } if err = chdir(".."); err != nil { - return errors.Wrap(err, 0) + return WrapError(err) } } } @@ -797,10 +797,25 @@ func (c *GitCommand) CherryPickCommits(commits []*Commit) error { return c.OSCommand.RunPreparedCommand(cmd) } -// CommitFiles get the specified commit files -func (c *GitCommand) CommitFiles(commitID string) (string, error) { +// GetCommitFiles get the specified commit files +func (c *GitCommand) GetCommitFiles(commitID string) ([]*CommitFile, error) { cmd := fmt.Sprintf("git show --pretty= --name-only %s", commitID) - return c.OSCommand.RunCommandWithOutput(cmd) + files, err := c.OSCommand.RunCommandWithOutput(cmd) + if err != nil { + return nil, err + } + + commitFiles := make([]*CommitFile, 0) + + for _, file := range strings.Split(strings.TrimRight(files, "\n"), "\n") { + commitFiles = append(commitFiles, &CommitFile{ + Sha: commitID, + Name: file, + DisplayString: file, + }) + } + + return commitFiles, nil } // ShowCommitFile get the diff of specified commit file @@ -814,3 +829,55 @@ func (c *GitCommand) CheckoutFile(commitSha, fileName string) error { cmd := fmt.Sprintf("git checkout %s %s", commitSha, fileName) return c.OSCommand.RunCommand(cmd) } + +// DiscardOldFileChanges discards changes to a file from an old commit +func (c *GitCommand) DiscardOldFileChanges(commits []*Commit, commitIndex int, fileName string) error { + // we can make this GPG thing possible it just means we need to do this in two parts: + // one where we handle the possibility of a credential request, and the other + // where we continue the rebase + if c.usingGpg() { + errors.New("feature not available for users using GPG") + } + + commitSha := commits[commitIndex].Sha + + todo, err := c.GenerateGenericRebaseTodo(commits, commitIndex, "edit") + if err != nil { + return err + } + + cmd, err := c.PrepareInteractiveRebaseCommand(commitSha+"^", todo, true) + if err != nil { + return err + } + + if err := c.OSCommand.RunPreparedCommand(cmd); err != nil { + return err + } + + // check if file exists in previous commit (this command returns an error if the file doesn't exist) + if err := c.OSCommand.RunCommand(fmt.Sprintf("git cat-file -e HEAD^:%s", fileName)); err != nil { + if err := c.OSCommand.RemoveFile(fileName); err != nil { + return err + } + if err := c.StageFile(fileName); err != nil { + return err + } + } else { + if err := c.CheckoutFile("HEAD^", fileName); err != nil { + return err + } + } + + // amend the commit + cmd, err = c.AmendHead() + if cmd != nil { + errors.New("received unexpected pointer to cmd") + } + if err != nil { + return err + } + + // continue + return c.GenericMerge("rebase", "continue") +} diff --git a/pkg/commands/os.go b/pkg/commands/os.go index a7a1fdaa4..9e0967cba 100644 --- a/pkg/commands/os.go +++ b/pkg/commands/os.go @@ -145,7 +145,7 @@ func sanitisedCommandOutput(output []byte, err error) (string, error) { // errors like 'exit status 1' are not very useful so we'll create an error // from the combined output if outputString == "" { - return "", errors.Wrap(err, 0) + return "", WrapError(err) } return outputString, errors.New(outputString) } @@ -224,13 +224,13 @@ func (c *OSCommand) Unquote(message string) string { func (c *OSCommand) AppendLineToFile(filename, line string) error { f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) if err != nil { - return errors.Wrap(err, 0) + return WrapError(err) } defer f.Close() _, err = f.WriteString("\n" + line) if err != nil { - return errors.Wrap(err, 0) + return WrapError(err) } return nil } @@ -240,16 +240,16 @@ func (c *OSCommand) CreateTempFile(filename, content string) (string, error) { tmpfile, err := ioutil.TempFile("", filename) if err != nil { c.Log.Error(err) - return "", errors.Wrap(err, 0) + return "", WrapError(err) } if _, err := tmpfile.WriteString(content); err != nil { c.Log.Error(err) - return "", errors.Wrap(err, 0) + return "", WrapError(err) } if err := tmpfile.Close(); err != nil { c.Log.Error(err) - return "", errors.Wrap(err, 0) + return "", WrapError(err) } return tmpfile.Name(), nil @@ -258,7 +258,7 @@ func (c *OSCommand) CreateTempFile(filename, content string) (string, error) { // RemoveFile removes a file at the specified path func (c *OSCommand) RemoveFile(filename string) error { err := os.Remove(filename) - return errors.Wrap(err, 0) + return WrapError(err) } // FileExists checks whether a file exists at the specified path diff --git a/pkg/gui/commit_files_panel.go b/pkg/gui/commit_files_panel.go index 1d54a2bcf..7feda9628 100644 --- a/pkg/gui/commit_files_panel.go +++ b/pkg/gui/commit_files_panel.go @@ -53,12 +53,49 @@ func (gui *Gui) handleSwitchToCommitsPanel(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleCheckoutCommitFile(g *gocui.Gui, v *gocui.View) error { - commitSha := gui.State.Commits[gui.State.Panels.Commits.SelectedLine].Sha - fileName := gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLine].Name + file := gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLine] - if err := gui.GitCommand.CheckoutFile(commitSha, fileName); err != nil { + if err := gui.GitCommand.CheckoutFile(file.Sha, file.Name); err != nil { return gui.createErrorPanel(gui.g, err.Error()) } return gui.refreshFiles() } + +func (gui *Gui) handleDiscardOldFileChange(g *gocui.Gui, v *gocui.View) error { + fileName := gui.State.CommitFiles[gui.State.Panels.CommitFiles.SelectedLine].Name + + return gui.createConfirmationPanel(gui.g, v, gui.Tr.SLocalize("DiscardFileChangesTitle"), gui.Tr.SLocalize("DiscardFileChangesPrompt"), func(g *gocui.Gui, v *gocui.View) error { + return gui.WithWaitingStatus(gui.Tr.SLocalize("RebasingStatus"), func() error { + if err := gui.GitCommand.DiscardOldFileChanges(gui.State.Commits, gui.State.Panels.Commits.SelectedLine, fileName); err != nil { + if err := gui.handleGenericMergeCommandResult(err); err != nil { + return err + } + } + + return gui.refreshSidePanels(gui.g) + }) + }, nil) +} + +func (gui *Gui) refreshCommitFilesView() error { + commit := gui.getSelectedCommit(gui.g) + if commit == nil { + return nil + } + + files, err := gui.GitCommand.GetCommitFiles(commit.Sha) + if err != nil { + return gui.createErrorPanel(gui.g, err.Error()) + } + + gui.State.CommitFiles = files + + gui.refreshSelectedLine(&gui.State.Panels.CommitFiles.SelectedLine, len(gui.State.CommitFiles)) + + if err := gui.renderListPanel(gui.getCommitFilesView(), gui.State.CommitFiles); err != nil { + return err + } + + return gui.handleCommitFileSelect(gui.g, gui.getCommitFilesView()) +} diff --git a/pkg/gui/commits_panel.go b/pkg/gui/commits_panel.go index 0de71e875..9b83d725d 100644 --- a/pkg/gui/commits_panel.go +++ b/pkg/gui/commits_panel.go @@ -3,7 +3,6 @@ package gui import ( "fmt" "strconv" - "strings" "github.com/go-errors/errors" @@ -72,9 +71,12 @@ func (gui *Gui) refreshCommits(g *gocui.Gui) error { fmt.Fprint(v, list) gui.refreshStatus(g) - if v == g.CurrentView() { + if g.CurrentView() == v { gui.handleCommitSelect(g, v) } + if g.CurrentView() == gui.getCommitFilesView() { + gui.refreshCommitFilesView() + } return nil }) return nil @@ -440,36 +442,9 @@ func (gui *Gui) HandlePasteCommits(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleSwitchToCommitFilesPanel(g *gocui.Gui, v *gocui.View) error { - commit := gui.getSelectedCommit(g) - if commit == nil { - return nil - } - - commitfileView, err := g.View("commitFiles") - if err != nil { + if err := gui.refreshCommitFilesView(); err != nil { return err } - files, err := gui.GitCommand.CommitFiles(commit.Sha) - if err != nil { - return gui.createErrorPanel(g, err.Error()) - } - - gui.State.Panels.CommitFiles = &commitFilesPanelState{SelectedLine: 0} - gui.State.CommitFiles = make([]*commands.CommitFile, 0) - - if files == "" { - gui.State.Panels.CommitFiles.SelectedLine = -1 - } - - for _, file := range strings.Split(strings.TrimRight(files, "\n"), "\n") { - gui.State.CommitFiles = append(gui.State.CommitFiles, &commands.CommitFile{ - Sha: commit.Sha, - Name: file, - DisplayString: file, - }) - } - - gui.renderListPanel(gui.getCommitFilesView(), gui.State.CommitFiles) - return gui.switchFocus(g, v, commitfileView) + return gui.switchFocus(g, v, gui.getCommitFilesView()) } diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index fc40cda07..bd1045105 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -155,11 +155,12 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *comma StashEntries: make([]*commands.StashEntry, 0), Platform: *oSCommand.Platform, Panels: &panelStates{ - Files: &filePanelState{SelectedLine: -1}, - Branches: &branchPanelState{SelectedLine: 0}, - Commits: &commitPanelState{SelectedLine: -1}, - Stash: &stashPanelState{SelectedLine: -1}, - Menu: &menuPanelState{SelectedLine: 0}, + Files: &filePanelState{SelectedLine: -1}, + Branches: &branchPanelState{SelectedLine: 0}, + Commits: &commitPanelState{SelectedLine: -1}, + CommitFiles: &commitFilesPanelState{SelectedLine: -1}, + Stash: &stashPanelState{SelectedLine: -1}, + Menu: &menuPanelState{SelectedLine: 0}, Merging: &mergingPanelState{ ConflictIndex: 0, ConflictTop: true, @@ -219,20 +220,21 @@ func max(a, b int) int { // getFocusLayout returns a manager function for when view gain and lose focus func (gui *Gui) getFocusLayout() func(g *gocui.Gui) error { - var focusedView *gocui.View + var previousView *gocui.View return func(g *gocui.Gui) error { - v := gui.g.CurrentView() - if v != focusedView { - if err := gui.onFocusChange(); err != nil { - return err - } - if err := gui.onFocusLost(focusedView); err != nil { + newView := gui.g.CurrentView() + if err := gui.onFocusChange(); err != nil { + return err + } + // for now we don't consider losing focus to a popup panel as actually losing focus + if newView != previousView && !gui.isPopupPanel(newView.Name()) { + if err := gui.onFocusLost(previousView, newView); err != nil { return err } - if err := gui.onFocus(v); err != nil { + if err := gui.onFocus(newView); err != nil { return err } - focusedView = v + previousView = newView } return nil } @@ -246,31 +248,23 @@ func (gui *Gui) onFocusChange() error { return gui.setMainTitle() } -func (gui *Gui) onFocusLost(v *gocui.View) error { +func (gui *Gui) onFocusLost(v *gocui.View, newView *gocui.View) error { if v == nil { return nil } if v.Name() == "branches" { + // This stops the branches panel from showing the upstream/downstream changes to the selected branch, when it loses focus + // inside renderListPanel it checks to see if the panel has focus if err := gui.renderListPanel(gui.getBranchesView(), gui.State.Branches); err != nil { return err } } else if v.Name() == "main" { - // if we have lost focus to a popup panel, that's okay - if gui.popupPanelFocused() { - return nil - } - // if we have lost focus to a first-class panel, we need to do some cleanup if err := gui.changeContext("main", "normal"); err != nil { return err } } else if v.Name() == "commitFiles" { - // if we have lost focus to a popup panel, that's okay - if gui.popupPanelFocused() { - return nil - } - gui.g.SetViewOnBottom(v.Name()) } gui.Log.Info(v.Name() + " focus lost") diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index c5eb758e9..d06280ef0 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -459,10 +459,16 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Modifier: gocui.ModNone, Handler: gui.handleCheckoutCommitFile, Description: gui.Tr.SLocalize("checkoutCommitFile"), + }, { + ViewName: "commitFiles", + Key: 'd', + Modifier: gocui.ModNone, + Handler: gui.handleDiscardOldFileChange, + Description: gui.Tr.SLocalize("discardOldFileChange"), }, } - for _, viewName := range []string{"status", "branches", "files", "commits", "stash", "menu"} { + for _, viewName := range []string{"status", "branches", "files", "commits", "commitFiles", "stash", "menu"} { bindings = append(bindings, []*Binding{ {ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.nextView}, {ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.previousView}, diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index eb46a7f8b..372c5d43e 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -22,6 +22,11 @@ func (gui *Gui) refreshSidePanels(g *gocui.Gui) error { if err := gui.refreshCommits(g); err != nil { return err } + + if err := gui.refreshCommitFilesView(); err != nil { + return err + } + return gui.refreshStashEntries(g) } @@ -30,8 +35,13 @@ func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error { if v == nil || v.Name() == cyclableViews[len(cyclableViews)-1] { focusedViewName = cyclableViews[0] } else { + // if we're in the commitFiles view we'll act like we're in the commits view + viewName := v.Name() + if viewName == "commitFiles" { + viewName = "commits" + } for i := range cyclableViews { - if v.Name() == cyclableViews[i] { + if viewName == cyclableViews[i] { focusedViewName = cyclableViews[i+1] break } @@ -39,7 +49,7 @@ func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error { message := gui.Tr.TemplateLocalize( "IssntListOfViews", Teml{ - "name": v.Name(), + "name": viewName, }, ) gui.Log.Info(message) @@ -59,8 +69,13 @@ func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error { if v == nil || v.Name() == cyclableViews[0] { focusedViewName = cyclableViews[len(cyclableViews)-1] } else { + // if we're in the commitFiles view we'll act like we're in the commits view + viewName := v.Name() + if viewName == "commitFiles" { + viewName = "commits" + } for i := range cyclableViews { - if v.Name() == cyclableViews[i] { + if viewName == cyclableViews[i] { focusedViewName = cyclableViews[i-1] // TODO: make this work properly break } @@ -68,7 +83,7 @@ func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error { message := gui.Tr.TemplateLocalize( "IssntListOfViews", Teml{ - "name": v.Name(), + "name": viewName, }, ) gui.Log.Info(message) @@ -131,15 +146,10 @@ func (gui *Gui) returnFocus(g *gocui.Gui, v *gocui.View) error { // pass in oldView = nil if you don't want to be able to return to your old view // TODO: move some of this logic into our onFocusLost and onFocus hooks func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error { - // we assume we'll never want to return focus to a confirmation panel i.e. - // we should never stack confirmation panels - if oldView != nil && oldView.Name() != "confirmation" { - // second class panels should never have focus restored to them because - // once they lose focus they are effectively 'destroyed' - secondClassPanels := []string{"confirmation", "menu"} - if !utils.IncludesString(secondClassPanels, oldView.Name()) { - gui.State.PreviousView = oldView.Name() - } + // we assume we'll never want to return focus to a popup panel i.e. + // we should never stack popup panels + if oldView != nil && !gui.isPopupPanel(oldView.Name()) { + gui.State.PreviousView = oldView.Name() } gui.Log.Info("setting highlight to true for view" + newView.Name()) @@ -301,7 +311,7 @@ func (gui *Gui) currentViewName() string { func (gui *Gui) resizeCurrentPopupPanel(g *gocui.Gui) error { v := g.CurrentView() - if v.Name() == "commitMessage" || v.Name() == "credentials" || v.Name() == "confirmation" { + if gui.isPopupPanel(v.Name()) { return gui.resizePopupPanel(g, v) } return nil @@ -393,14 +403,10 @@ func (gui *Gui) handleFocusView(g *gocui.Gui, v *gocui.View) error { return err } +func (gui *Gui) isPopupPanel(viewName string) bool { + return viewName == "commitMessage" || viewName == "credentials" || viewName == "confirmation" || viewName == "menu" +} + func (gui *Gui) popupPanelFocused() bool { - viewNames := []string{"commitMessage", - "credentials", - "menu"} - for _, viewName := range viewNames { - if gui.currentViewName() == viewName { - return true - } - } - return false + return gui.isPopupPanel(gui.currentViewName()) } diff --git a/pkg/i18n/dutch.go b/pkg/i18n/dutch.go index 4d2ee4236..13eb1ee0c 100644 --- a/pkg/i18n/dutch.go +++ b/pkg/i18n/dutch.go @@ -658,6 +658,15 @@ func addDutch(i18nObject *i18n.Bundle) error { }, &i18n.Message{ ID: "checkoutCommitFile", Other: "checkout file", + }, &i18n.Message{ + ID: "discardOldFileChange", + Other: "discard this commit's changes to this file", + }, &i18n.Message{ + ID: "DiscardFileChangesTitle", + Other: "Discard file changes", + }, &i18n.Message{ + ID: "DiscardFileChangesPrompt", + Other: "Are you sure you want to discard this commit's changes to this file? If this file was created in this commit, it will be deleted", }, ) } diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index c9eda4ce6..925b9db38 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -681,6 +681,15 @@ func addEnglish(i18nObject *i18n.Bundle) error { }, &i18n.Message{ ID: "checkoutCommitFile", Other: "checkout file", + }, &i18n.Message{ + ID: "discardOldFileChange", + Other: "discard this commit's changes to this file", + }, &i18n.Message{ + ID: "DiscardFileChangesTitle", + Other: "Discard file changes", + }, &i18n.Message{ + ID: "DiscardFileChangesPrompt", + Other: "Are you sure you want to discard this commit's changes to this file? If this file was created in this commit, it will be deleted", }, ) } diff --git a/pkg/i18n/polish.go b/pkg/i18n/polish.go index 30462a7c3..37791d9e5 100644 --- a/pkg/i18n/polish.go +++ b/pkg/i18n/polish.go @@ -641,6 +641,15 @@ func addPolish(i18nObject *i18n.Bundle) error { }, &i18n.Message{ ID: "checkoutCommitFile", Other: "checkout file", + }, &i18n.Message{ + ID: "discardOldFileChange", + Other: "discard this commit's changes to this file", + }, &i18n.Message{ + ID: "DiscardFileChangesTitle", + Other: "Discard file changes", + }, &i18n.Message{ + ID: "DiscardFileChangesPrompt", + Other: "Are you sure you want to discard this commit's changes to this file? If this file was created in this commit, it will be deleted", }, ) } |