From 6afcc5bda8bbe9d9976fd50740564fe201c3ea41 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 17 May 2024 19:04:21 +0200 Subject: Create shims for all model classes in SessionStateLoader This guards against accidentally renaming a model field and thereby breaking user's custom commands. With this change we'll get a build failure when we do that. --- docs/Custom_Command_Keybindings.md | 2 +- pkg/gui/services/custom_commands/models.go | 96 +++++++++++ .../custom_commands/session_state_loader.go | 178 ++++++++++++++++----- 3 files changed, 237 insertions(+), 39 deletions(-) create mode 100644 pkg/gui/services/custom_commands/models.go diff --git a/docs/Custom_Command_Keybindings.md b/docs/Custom_Command_Keybindings.md index 8604a5247..426d6f8f6 100644 --- a/docs/Custom_Command_Keybindings.md +++ b/docs/Custom_Command_Keybindings.md @@ -305,7 +305,7 @@ SelectedWorktree CheckedOutBranch ``` -To see what fields are available on e.g. the `SelectedFile`, see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/commands/models/file.go) (all the modelling lives in the same directory). Note that the custom commands feature does not guarantee backwards compatibility (until we hit Lazygit version 1.0 of course) which means a field you're accessing on an object may no longer be available from one release to the next. Typically however, all you'll need is `{{.SelectedFile.Name}}`, `{{.SelectedLocalCommit.Hash}}` and `{{.SelectedLocalBranch.Name}}`. In the future we will likely introduce a tighter interface that exposes a limited set of fields for each model. +To see what fields are available on e.g. the `SelectedFile`, see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/gui/services/custom_commands/models.go) (all the modelling lives in the same file). ## Keybinding collisions diff --git a/pkg/gui/services/custom_commands/models.go b/pkg/gui/services/custom_commands/models.go new file mode 100644 index 000000000..a33a4daf8 --- /dev/null +++ b/pkg/gui/services/custom_commands/models.go @@ -0,0 +1,96 @@ +package custom_commands + +import ( + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/stefanhaller/git-todo-parser/todo" +) + +// We create shims for all the model classes in order to get a more stable API +// for custom commands. At the moment these are almost identical to the model +// classes, but this allows us to add "private" fields to the model classes that +// we don't want to expose to custom commands, or rename a model field to a +// better name without breaking people's custom commands. In such a case we add +// the new, better name to the shim but keep the old one for backwards +// compatibility. We already did this for Commit.Sha, which was renamed to Hash. + +type Commit struct { + Hash string // deprecated: use Sha + Sha string + Name string + Status models.CommitStatus + Action todo.TodoCommand + Tags []string + ExtraInfo string + AuthorName string + AuthorEmail string + UnixTimestamp int64 + Divergence models.Divergence + Parents []string +} + +type File struct { + Name string + PreviousName string + HasStagedChanges bool + HasUnstagedChanges bool + Tracked bool + Added bool + Deleted bool + HasMergeConflicts bool + HasInlineMergeConflicts bool + DisplayString string + ShortStatus string + IsWorktree bool +} + +type Branch struct { + Name string + DisplayName string + Recency string + Pushables string + Pullables string + UpstreamGone bool + Head bool + DetachedHead bool + UpstreamRemote string + UpstreamBranch string + Subject string + CommitHash string +} + +type RemoteBranch struct { + Name string + RemoteName string +} + +type Remote struct { + Name string + Urls []string + Branches []*RemoteBranch +} + +type Tag struct { + Name string + Message string +} + +type StashEntry struct { + Index int + Recency string + Name string +} + +type CommitFile struct { + Name string + ChangeStatus string +} + +type Worktree struct { + IsMain bool + IsCurrent bool + Path string + IsPathMissing bool + GitDir string + Branch string + Name string +} diff --git a/pkg/gui/services/custom_commands/session_state_loader.go b/pkg/gui/services/custom_commands/session_state_loader.go index 6a3068df8..23369cb0b 100644 --- a/pkg/gui/services/custom_commands/session_state_loader.go +++ b/pkg/gui/services/custom_commands/session_state_loader.go @@ -3,7 +3,7 @@ package custom_commands import ( "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" - "github.com/stefanhaller/git-todo-parser/todo" + "github.com/samber/lo" ) // loads the session state at the time that a custom command is invoked, for use @@ -20,22 +20,7 @@ func NewSessionStateLoader(c *helpers.HelperCommon, refsHelper *helpers.RefsHelp } } -type Commit struct { - Hash string - Sha string - Name string - Status models.CommitStatus - Action todo.TodoCommand - Tags []string - ExtraInfo string - AuthorName string - AuthorEmail string - UnixTimestamp int64 - Divergence models.Divergence - Parents []string -} - -func commitWrapperFromModelCommit(commit *models.Commit) *Commit { +func commitShimFromModelCommit(commit *models.Commit) *Commit { if commit == nil { return nil } @@ -56,39 +41,156 @@ func commitWrapperFromModelCommit(commit *models.Commit) *Commit { } } +func fileShimFromModelFile(file *models.File) *File { + if file == nil { + return nil + } + + return &File{ + Name: file.Name, + PreviousName: file.PreviousName, + HasStagedChanges: file.HasStagedChanges, + HasUnstagedChanges: file.HasUnstagedChanges, + Tracked: file.Tracked, + Added: file.Added, + Deleted: file.Deleted, + HasMergeConflicts: file.HasMergeConflicts, + HasInlineMergeConflicts: file.HasInlineMergeConflicts, + DisplayString: file.DisplayString, + ShortStatus: file.ShortStatus, + IsWorktree: file.IsWorktree, + } +} + +func branchShimFromModelBranch(branch *models.Branch) *Branch { + if branch == nil { + return nil + } + + return &Branch{ + Name: branch.Name, + DisplayName: branch.DisplayName, + Recency: branch.Recency, + Pushables: branch.Pushables, + Pullables: branch.Pullables, + UpstreamGone: branch.UpstreamGone, + Head: branch.Head, + DetachedHead: branch.DetachedHead, + UpstreamRemote: branch.UpstreamRemote, + UpstreamBranch: branch.UpstreamBranch, + Subject: branch.Subject, + CommitHash: branch.CommitHash, + } +} + +func remoteBranchShimFromModelRemoteBranch(remoteBranch *models.RemoteBranch) *RemoteBranch { + if remoteBranch == nil { + return nil + } + + return &RemoteBranch{ + Name: remoteBranch.Name, + RemoteName: remoteBranch.RemoteName, + } +} + +func remoteShimFromModelRemote(remote *models.Remote) *Remote { + if remote == nil { + return nil + } + + return &Remote{ + Name: remote.Name, + Urls: remote.Urls, + Branches: lo.Map(remote.Branches, func(branch *models.RemoteBranch, _ int) *RemoteBranch { + return remoteBranchShimFromModelRemoteBranch(branch) + }), + } +} + +func tagShimFromModelRemote(tag *models.Tag) *Tag { + if tag == nil { + return nil + } + + return &Tag{ + Name: tag.Name, + Message: tag.Message, + } +} + +func stashEntryShimFromModelRemote(stashEntry *models.StashEntry) *StashEntry { + if stashEntry == nil { + return nil + } + + return &StashEntry{ + Index: stashEntry.Index, + Recency: stashEntry.Recency, + Name: stashEntry.Name, + } +} + +func commitFileShimFromModelRemote(commitFile *models.CommitFile) *CommitFile { + if commitFile == nil { + return nil + } + + return &CommitFile{ + Name: commitFile.Name, + ChangeStatus: commitFile.ChangeStatus, + } +} + +func worktreeShimFromModelRemote(worktree *models.Worktree) *Worktree { + if worktree == nil { + return nil + } + + return &Worktree{ + IsMain: worktree.IsMain, + IsCurrent: worktree.IsCurrent, + Path: worktree.Path, + IsPathMissing: worktree.IsPathMissing, + GitDir: worktree.GitDir, + Branch: worktree.Branch, + Name: worktree.Name, + } +} + // SessionState captures the current state of the application for use in custom commands type SessionState struct { SelectedLocalCommit *Commit SelectedReflogCommit *Commit SelectedSubCommit *Commit - SelectedFile *models.File + SelectedFile *File SelectedPath string - SelectedLocalBranch *models.Branch - SelectedRemoteBranch *models.RemoteBranch - SelectedRemote *models.Remote - SelectedTag *models.Tag - SelectedStashEntry *models.StashEntry - SelectedCommitFile *models.CommitFile + SelectedLocalBranch *Branch + SelectedRemoteBranch *RemoteBranch + SelectedRemote *Remote + SelectedTag *Tag + SelectedStashEntry *StashEntry + SelectedCommitFile *CommitFile SelectedCommitFilePath string - SelectedWorktree *models.Worktree - CheckedOutBranch *models.Branch + SelectedWorktree *Worktree + CheckedOutBranch *Branch } func (self *SessionStateLoader) call() *SessionState { return &SessionState{ - SelectedFile: self.c.Contexts().Files.GetSelectedFile(), + SelectedFile: fileShimFromModelFile(self.c.Contexts().Files.GetSelectedFile()), SelectedPath: self.c.Contexts().Files.GetSelectedPath(), - SelectedLocalCommit: commitWrapperFromModelCommit(self.c.Contexts().LocalCommits.GetSelected()), - SelectedReflogCommit: commitWrapperFromModelCommit(self.c.Contexts().ReflogCommits.GetSelected()), - SelectedLocalBranch: self.c.Contexts().Branches.GetSelected(), - SelectedRemoteBranch: self.c.Contexts().RemoteBranches.GetSelected(), - SelectedRemote: self.c.Contexts().Remotes.GetSelected(), - SelectedTag: self.c.Contexts().Tags.GetSelected(), - SelectedStashEntry: self.c.Contexts().Stash.GetSelected(), - SelectedCommitFile: self.c.Contexts().CommitFiles.GetSelectedFile(), + SelectedLocalCommit: commitShimFromModelCommit(self.c.Contexts().LocalCommits.GetSelected()), + SelectedReflogCommit: commitShimFromModelCommit(self.c.Contexts().ReflogCommits.GetSelected()), + SelectedLocalBranch: branchShimFromModelBranch(self.c.Contexts().Branches.GetSelected()), + SelectedRemoteBranch: remoteBranchShimFromModelRemoteBranch(self.c.Contexts().RemoteBranches.GetSelected()), + SelectedRemote: remoteShimFromModelRemote(self.c.Contexts().Remotes.GetSelected()), + SelectedTag: tagShimFromModelRemote(self.c.Contexts().Tags.GetSelected()), + SelectedStashEntry: stashEntryShimFromModelRemote(self.c.Contexts().Stash.GetSelected()), + SelectedCommitFile: commitFileShimFromModelRemote(self.c.Contexts().CommitFiles.GetSelectedFile()), SelectedCommitFilePath: self.c.Contexts().CommitFiles.GetSelectedPath(), - SelectedSubCommit: commitWrapperFromModelCommit(self.c.Contexts().SubCommits.GetSelected()), - SelectedWorktree: self.c.Contexts().Worktrees.GetSelected(), - CheckedOutBranch: self.refsHelper.GetCheckedOutRef(), + SelectedSubCommit: commitShimFromModelCommit(self.c.Contexts().SubCommits.GetSelected()), + SelectedWorktree: worktreeShimFromModelRemote(self.c.Contexts().Worktrees.GetSelected()), + CheckedOutBranch: branchShimFromModelBranch(self.refsHelper.GetCheckedOutRef()), } } -- cgit v1.2.3 From c4927e21c560c98b147ee3e9912366e69b113011 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 17 May 2024 21:08:53 +0200 Subject: Rename PushBranch to PushBranchAndSetUpstream It is unexpected that a function called PushBranch also sets the upstream branch; also, we want to add a PushBranch function in the next commit that doesn't. --- pkg/integration/components/shell.go | 2 +- pkg/integration/tests/branch/delete.go | 4 ++-- .../tests/branch/delete_remote_branch_with_credential_prompt.go | 2 +- pkg/integration/tests/branch/rebase_to_upstream.go | 2 +- pkg/integration/tests/branch/reset_to_upstream.go | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/integration/components/shell.go b/pkg/integration/components/shell.go index e3df61a50..8d18361fb 100644 --- a/pkg/integration/components/shell.go +++ b/pkg/integration/components/shell.go @@ -194,7 +194,7 @@ func (self *Shell) CreateAnnotatedTag(name string, message string, ref string) * return self.RunCommand([]string{"git", "tag", "-a", name, "-m", message, ref}) } -func (self *Shell) PushBranch(upstream, branch string) *Shell { +func (self *Shell) PushBranchAndSetUpstream(upstream, branch string) *Shell { return self.RunCommand([]string{"git", "push", "--set-upstream", upstream, branch}) } diff --git a/pkg/integration/tests/branch/delete.go b/pkg/integration/tests/branch/delete.go index 0b6adfac4..f81eb0609 100644 --- a/pkg/integration/tests/branch/delete.go +++ b/pkg/integration/tests/branch/delete.go @@ -15,9 +15,9 @@ var Delete = NewIntegrationTest(NewIntegrationTestArgs{ CloneIntoRemote("origin"). EmptyCommit("blah"). NewBranch("branch-one"). - PushBranch("origin", "branch-one"). + PushBranchAndSetUpstream("origin", "branch-one"). NewBranch("branch-two"). - PushBranch("origin", "branch-two"). + PushBranchAndSetUpstream("origin", "branch-two"). EmptyCommit("deletion blocker"). NewBranch("branch-three") }, diff --git a/pkg/integration/tests/branch/delete_remote_branch_with_credential_prompt.go b/pkg/integration/tests/branch/delete_remote_branch_with_credential_prompt.go index f145eceaa..ad8f70c43 100644 --- a/pkg/integration/tests/branch/delete_remote_branch_with_credential_prompt.go +++ b/pkg/integration/tests/branch/delete_remote_branch_with_credential_prompt.go @@ -18,7 +18,7 @@ var DeleteRemoteBranchWithCredentialPrompt = NewIntegrationTest(NewIntegrationTe shell.NewBranch("mybranch") - shell.PushBranch("origin", "mybranch") + shell.PushBranchAndSetUpstream("origin", "mybranch") // actually getting a password prompt is tricky: it requires SSH'ing into localhost under a newly created, restricted, user. // This is not easy to do in a cross-platform way, nor is it easy to do in a docker container. diff --git a/pkg/integration/tests/branch/rebase_to_upstream.go b/pkg/integration/tests/branch/rebase_to_upstream.go index 5c9e0f388..2469eb012 100644 --- a/pkg/integration/tests/branch/rebase_to_upstream.go +++ b/pkg/integration/tests/branch/rebase_to_upstream.go @@ -15,7 +15,7 @@ var RebaseToUpstream = NewIntegrationTest(NewIntegrationTestArgs{ CloneIntoRemote("origin"). EmptyCommit("ensure-master"). EmptyCommit("to-be-added"). // <- this will only exist remotely - PushBranch("origin", "master"). + PushBranchAndSetUpstream("origin", "master"). HardReset("HEAD~1"). NewBranchFrom("base-branch", "master"). EmptyCommit("base-branch-commit"). diff --git a/pkg/integration/tests/branch/reset_to_upstream.go b/pkg/integration/tests/branch/reset_to_upstream.go index c933787e4..3cdbb561d 100644 --- a/pkg/integration/tests/branch/reset_to_upstream.go +++ b/pkg/integration/tests/branch/reset_to_upstream.go @@ -15,10 +15,10 @@ var ResetToUpstream = NewIntegrationTest(NewIntegrationTestArgs{ CloneIntoRemote("origin"). NewBranch("hard-branch"). EmptyCommit("hard commit"). - PushBranch("origin", "hard-branch"). + PushBranchAndSetUpstream("origin", "hard-branch"). NewBranch("soft-branch"). EmptyCommit("soft commit"). - PushBranch("origin", "soft-branch"). + PushBranchAndSetUpstream("origin", "soft-branch"). NewBranch("base"). EmptyCommit("base-branch commit"). CreateFile("file-1", "content"). -- cgit v1.2.3 From b91b40ba4d8d969d92228829481c2b517aa124d2 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 17 May 2024 21:07:38 +0200 Subject: Add test demonstrating the problem with force-pushing in a triangular workflow Our code doesn't realize that we need to prompt the user to force push, when the branch is up-to-date with its upstream but not with the branch that we're pushing to. --- pkg/integration/components/shell.go | 4 ++ .../tests/sync/force_push_triangular.go | 72 ++++++++++++++++++++++ pkg/integration/tests/test_list.go | 1 + 3 files changed, 77 insertions(+) create mode 100644 pkg/integration/tests/sync/force_push_triangular.go diff --git a/pkg/integration/components/shell.go b/pkg/integration/components/shell.go index 8d18361fb..a8caff77d 100644 --- a/pkg/integration/components/shell.go +++ b/pkg/integration/components/shell.go @@ -194,6 +194,10 @@ func (self *Shell) CreateAnnotatedTag(name string, message string, ref string) * return self.RunCommand([]string{"git", "tag", "-a", name, "-m", message, ref}) } +func (self *Shell) PushBranch(upstream, branch string) *Shell { + return self.RunCommand([]string{"git", "push", upstream, branch}) +} + func (self *Shell) PushBranchAndSetUpstream(upstream, branch string) *Shell { return self.RunCommand([]string{"git", "push", "--set-upstream", upstream, branch}) } diff --git a/pkg/integration/tests/sync/force_push_triangular.go b/pkg/integration/tests/sync/force_push_triangular.go new file mode 100644 index 000000000..379912f6c --- /dev/null +++ b/pkg/integration/tests/sync/force_push_triangular.go @@ -0,0 +1,72 @@ +package sync + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var ForcePushTriangular = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Push to a remote, requiring a force push because the branch is behind the remote push branch but not the upstream", + ExtraCmdArgs: []string{}, + Skip: false, + GitVersion: AtLeast("2.22.0"), + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.SetConfig("push.default", "current") + + shell.EmptyCommit("one") + + shell.CloneIntoRemote("origin") + + shell.NewBranch("feature") + shell.SetBranchUpstream("feature", "origin/master") + shell.EmptyCommit("two") + shell.PushBranch("origin", "feature") + + // remove the 'two' commit so that we are behind the push branch + shell.HardReset("HEAD^") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Lines( + Contains("one"), + ) + + t.Views().Status().Content(Contains("✓ repo → feature")) + + t.Views().Files().IsFocused().Press(keys.Universal.Push) + + // This results in an attempt to push normally, which fails with an error: + t.ExpectPopup().Alert(). + Title(Equals("Error")). + Content(Contains("Updates were rejected. Please fetch and examine the remote changes before pushing again.")) + + /* EXPECTED: + t.ExpectPopup().Confirmation(). + Title(Equals("Force push")). + Content(Equals("Your branch has diverged from the remote branch. Press to cancel, or to force push.")). + Confirm() + + t.Views().Commits(). + Lines( + Contains("one"), + ) + + t.Views().Status().Content(Contains("✓ repo → feature")) + + t.Views().Remotes().Focus(). + Lines(Contains("origin")). + PressEnter() + + t.Views().RemoteBranches().IsFocused(). + Lines( + Contains("feature"), + Contains("master"), + ). + PressEnter() + + t.Views().SubCommits().IsFocused(). + Lines(Contains("one")) + */ + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 7084fd99b..e043d4a8a 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -275,6 +275,7 @@ var tests = []*components.IntegrationTest{ sync.ForcePush, sync.ForcePushMultipleMatching, sync.ForcePushMultipleUpstream, + sync.ForcePushTriangular, sync.Pull, sync.PullAndSetUpstream, sync.PullMerge, -- cgit v1.2.3 From 0aba686f97f537301a545c888393a65b922478d2 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 17 May 2024 20:14:45 +0200 Subject: Rename Pushables/Pullables to AheadForPull/BehindForPull In preparation for adding AheadForPush/BehindForPush in the next commit. --- pkg/commands/git_commands/branch_loader.go | 24 +++---- pkg/commands/git_commands/branch_loader_test.go | 76 +++++++++++----------- pkg/commands/models/branch.go | 22 +++---- pkg/gui/controllers/branches_controller.go | 2 +- pkg/gui/controllers/sync_controller.go | 4 +- pkg/gui/presentation/branches.go | 8 +-- pkg/gui/presentation/branches_test.go | 24 +++---- pkg/gui/services/custom_commands/models.go | 6 +- .../custom_commands/session_state_loader.go | 6 +- 9 files changed, 88 insertions(+), 84 deletions(-) diff --git a/pkg/commands/git_commands/branch_loader.go b/pkg/commands/git_commands/branch_loader.go index 198368502..f1c68cd53 100644 --- a/pkg/commands/git_commands/branch_loader.go +++ b/pkg/commands/git_commands/branch_loader.go @@ -199,7 +199,7 @@ func obtainBranch(split []string, storeCommitDateAsRecency bool) *models.Branch commitDate := split[6] name := strings.TrimPrefix(fullName, "heads/") - pushables, pullables, gone := parseUpstreamInfo(upstreamName, track) + aheadForPull, behindForPull, gone := parseUpstreamInfo(upstreamName, track) recency := "" if storeCommitDateAsRecency { @@ -209,14 +209,14 @@ func obtainBranch(split []string, storeCommitDateAsRecency bool) *models.Branch } return &models.Branch{ - Name: name, - Recency: recency, - Pushables: pushables, - Pullables: pullables, - UpstreamGone: gone, - Head: headMarker == "*", - Subject: subject, - CommitHash: commitHash, + Name: name, + Recency: recency, + AheadForPull: aheadForPull, + BehindForPull: behindForPull, + UpstreamGone: gone, + Head: headMarker == "*", + Subject: subject, + CommitHash: commitHash, } } @@ -232,10 +232,10 @@ func parseUpstreamInfo(upstreamName string, track string) (string, string, bool) return "?", "?", true } - pushables := parseDifference(track, `ahead (\d+)`) - pullables := parseDifference(track, `behind (\d+)`) + ahead := parseDifference(track, `ahead (\d+)`) + behind := parseDifference(track, `behind (\d+)`) - return pushables, pullables, false + return ahead, behind, false } func parseDifference(track string, regexStr string) string { diff --git a/pkg/commands/git_commands/branch_loader_test.go b/pkg/commands/git_commands/branch_loader_test.go index 9e56666fe..e9b8001b5 100644 --- a/pkg/commands/git_commands/branch_loader_test.go +++ b/pkg/commands/git_commands/branch_loader_test.go @@ -28,12 +28,12 @@ func TestObtainBranch(t *testing.T) { input: []string{"", "heads/a_branch", "", "", "subject", "123", timeStamp}, storeCommitDateAsRecency: false, expectedBranch: &models.Branch{ - Name: "a_branch", - Pushables: "?", - Pullables: "?", - Head: false, - Subject: "subject", - CommitHash: "123", + Name: "a_branch", + AheadForPull: "?", + BehindForPull: "?", + Head: false, + Subject: "subject", + CommitHash: "123", }, }, { @@ -41,12 +41,12 @@ func TestObtainBranch(t *testing.T) { input: []string{"", "a_branch", "", "", "subject", "123", timeStamp}, storeCommitDateAsRecency: false, expectedBranch: &models.Branch{ - Name: "a_branch", - Pushables: "?", - Pullables: "?", - Head: false, - Subject: "subject", - CommitHash: "123", + Name: "a_branch", + AheadForPull: "?", + BehindForPull: "?", + Head: false, + Subject: "subject", + CommitHash: "123", }, }, { @@ -54,12 +54,12 @@ func TestObtainBranch(t *testing.T) { input: []string{"*", "a_branch", "", "", "subject", "123", timeStamp}, storeCommitDateAsRecency: false, expectedBranch: &models.Branch{ - Name: "a_branch", - Pushables: "?", - Pullables: "?", - Head: true, - Subject: "subject", - CommitHash: "123", + Name: "a_branch", + AheadForPull: "?", + BehindForPull: "?", + Head: true, + Subject: "subject", + CommitHash: "123", }, }, { @@ -67,12 +67,12 @@ func TestObtainBranch(t *testing.T) { input: []string{"", "a_branch", "a_remote/a_branch", "[behind 2, ahead 3]", "subject", "123", timeStamp}, storeCommitDateAsRecency: false, expectedBranch: &models.Branch{ - Name: "a_branch", - Pushables: "3", - Pullables: "2", - Head: false, - Subject: "subject", - CommitHash: "123", + Name: "a_branch", + AheadForPull: "3", + BehindForPull: "2", + Head: false, + Subject: "subject", + CommitHash: "123", }, }, { @@ -80,13 +80,13 @@ func TestObtainBranch(t *testing.T) { input: []string{"", "a_branch", "a_remote/a_branch", "[gone]", "subject", "123", timeStamp}, storeCommitDateAsRecency: false, expectedBranch: &models.Branch{ - Name: "a_branch", - UpstreamGone: true, - Pushables: "?", - Pullables: "?", - Head: false, - Subject: "subject", - CommitHash: "123", + Name: "a_branch", + UpstreamGone: true, + AheadForPull: "?", + BehindForPull: "?", + Head: false, + Subject: "subject", + CommitHash: "123", }, }, { @@ -94,13 +94,13 @@ func TestObtainBranch(t *testing.T) { input: []string{"", "a_branch", "", "", "subject", "123", timeStamp}, storeCommitDateAsRecency: true, expectedBranch: &models.Branch{ - Name: "a_branch", - Recency: "2h", - Pushables: "?", - Pullables: "?", - Head: false, - Subject: "subject", - CommitHash: "123", + Name: "a_branch", + Recency: "2h", + AheadForPull: "?", + BehindForPull: "?", + Head: false, + Subject: "subject", + CommitHash: "123", }, }, } diff --git a/pkg/commands/models/branch.go b/pkg/commands/models/branch.go index c5fcfdaed..9e4772410 100644 --- a/pkg/commands/models/branch.go +++ b/pkg/commands/models/branch.go @@ -10,10 +10,10 @@ type Branch struct { DisplayName string // indicator of when the branch was last checked out e.g. '2d', '3m' Recency string - // how many commits ahead we are from the remote branch (how many commits we can push) - Pushables string + // how many commits ahead we are from the remote branch (how many commits we can push, assuming we push to our tracked remote branch) + AheadForPull string // how many commits behind we are from the remote branch (how many commits we can pull) - Pullables string + BehindForPull string // whether the remote branch is 'gone' i.e. we're tracking a remote branch that has been deleted UpstreamGone bool // whether this is the current branch. Exactly one branch should have this be true @@ -80,26 +80,26 @@ func (b *Branch) IsTrackingRemote() bool { // we know that the remote branch is not stored locally based on our pushable/pullable // count being question marks. func (b *Branch) RemoteBranchStoredLocally() bool { - return b.IsTrackingRemote() && b.Pushables != "?" && b.Pullables != "?" + return b.IsTrackingRemote() && b.AheadForPull != "?" && b.BehindForPull != "?" } func (b *Branch) RemoteBranchNotStoredLocally() bool { - return b.IsTrackingRemote() && b.Pushables == "?" && b.Pullables == "?" + return b.IsTrackingRemote() && b.AheadForPull == "?" && b.BehindForPull == "?" } func (b *Branch) MatchesUpstream() bool { - return b.RemoteBranchStoredLocally() && b.Pushables == "0" && b.Pullables == "0" + return b.RemoteBranchStoredLocally() && b.AheadForPull == "0" && b.BehindForPull == "0" } -func (b *Branch) HasCommitsToPush() bool { - return b.RemoteBranchStoredLocally() && b.Pushables != "0" +func (b *Branch) IsAheadForPull() bool { + return b.RemoteBranchStoredLocally() && b.AheadForPull != "0" } -func (b *Branch) HasCommitsToPull() bool { - return b.RemoteBranchStoredLocally() && b.Pullables != "0" +func (b *Branch) IsBehindForPull() bool { + return b.RemoteBranchStoredLocally() && b.BehindForPull != "0" } // for when we're in a detached head state func (b *Branch) IsRealBranch() bool { - return b.Pushables != "" && b.Pullables != "" + return b.AheadForPull != "" && b.BehindForPull != "" } diff --git a/pkg/gui/controllers/branches_controller.go b/pkg/gui/controllers/branches_controller.go index b08ddd0cd..d7faa7811 100644 --- a/pkg/gui/controllers/branches_controller.go +++ b/pkg/gui/controllers/branches_controller.go @@ -620,7 +620,7 @@ func (self *BranchesController) fastForward(branch *models.Branch) error { if !branch.RemoteBranchStoredLocally() { return errors.New(self.c.Tr.FwdNoLocalUpstream) } - if branch.HasCommitsToPush() { + if branch.IsAheadForPull() { return errors.New(self.c.Tr.FwdCommitsToPush) } diff --git a/pkg/gui/controllers/sync_controller.go b/pkg/gui/controllers/sync_controller.go index 403f31d94..8bd8dac9a 100644 --- a/pkg/gui/controllers/sync_controller.go +++ b/pkg/gui/controllers/sync_controller.go @@ -87,10 +87,10 @@ func (self *SyncController) branchCheckedOut(f func(*models.Branch) error) func( } func (self *SyncController) push(currentBranch *models.Branch) error { - // if we have pullables we'll ask if the user wants to force push + // if we are behind our upstream branch we'll ask if the user wants to force push if currentBranch.IsTrackingRemote() { opts := pushOpts{} - if currentBranch.HasCommitsToPull() { + if currentBranch.IsBehindForPull() { return self.requestToForcePush(currentBranch, opts) } else { return self.pushAux(currentBranch, opts) diff --git a/pkg/gui/presentation/branches.go b/pkg/gui/presentation/branches.go index 0abf2d4cd..406a580d5 100644 --- a/pkg/gui/presentation/branches.go +++ b/pkg/gui/presentation/branches.go @@ -196,11 +196,11 @@ func BranchStatus( } result := "" - if branch.HasCommitsToPush() { - result = fmt.Sprintf("↑%s", branch.Pushables) + if branch.IsAheadForPull() { + result = fmt.Sprintf("↑%s", branch.AheadForPull) } - if branch.HasCommitsToPull() { - result = fmt.Sprintf("%s↓%s", result, branch.Pullables) + if branch.IsBehindForPull() { + result = fmt.Sprintf("%s↓%s", result, branch.BehindForPull) } return result diff --git a/pkg/gui/presentation/branches_test.go b/pkg/gui/presentation/branches_test.go index 250b143e3..cf2f1d994 100644 --- a/pkg/gui/presentation/branches_test.go +++ b/pkg/gui/presentation/branches_test.go @@ -58,8 +58,8 @@ func Test_getBranchDisplayStrings(t *testing.T) { Name: "branch_name", Recency: "1m", UpstreamRemote: "origin", - Pushables: "0", - Pullables: "0", + AheadForPull: "0", + BehindForPull: "0", }, itemOperation: types.ItemOperationNone, fullDescription: false, @@ -73,8 +73,8 @@ func Test_getBranchDisplayStrings(t *testing.T) { Name: "branch_name", Recency: "1m", UpstreamRemote: "origin", - Pushables: "3", - Pullables: "5", + AheadForPull: "3", + BehindForPull: "5", }, itemOperation: types.ItemOperationNone, fullDescription: false, @@ -99,8 +99,8 @@ func Test_getBranchDisplayStrings(t *testing.T) { CommitHash: "1234567890", UpstreamRemote: "origin", UpstreamBranch: "branch_name", - Pushables: "0", - Pullables: "0", + AheadForPull: "0", + BehindForPull: "0", Subject: "commit title", }, itemOperation: types.ItemOperationNone, @@ -144,8 +144,8 @@ func Test_getBranchDisplayStrings(t *testing.T) { Name: "branch_name", Recency: "1m", UpstreamRemote: "origin", - Pushables: "0", - Pullables: "0", + AheadForPull: "0", + BehindForPull: "0", }, itemOperation: types.ItemOperationNone, fullDescription: false, @@ -159,8 +159,8 @@ func Test_getBranchDisplayStrings(t *testing.T) { Name: "branch_name", Recency: "1m", UpstreamRemote: "origin", - Pushables: "3", - Pullables: "5", + AheadForPull: "3", + BehindForPull: "5", }, itemOperation: types.ItemOperationNone, fullDescription: false, @@ -212,8 +212,8 @@ func Test_getBranchDisplayStrings(t *testing.T) { CommitHash: "1234567890", UpstreamRemote: "origin", UpstreamBranch: "branch_name", - Pushables: "0", - Pullables: "0", + AheadForPull: "0", + BehindForPull: "0", Subject: "commit title", }, itemOperation: types.ItemOperationNone, diff --git a/pkg/gui/services/custom_commands/models.go b/pkg/gui/services/custom_commands/models.go index a33a4daf8..bd3a69224 100644 --- a/pkg/gui/services/custom_commands/models.go +++ b/pkg/gui/services/custom_commands/models.go @@ -47,8 +47,10 @@ type Branch struct { Name string DisplayName string Recency string - Pushables string - Pullables string + Pushables string // deprecated: use AheadForPull + Pullables string // deprecated: use BehindForPull + AheadForPull string + BehindForPull string UpstreamGone bool Head bool DetachedHead bool diff --git a/pkg/gui/services/custom_commands/session_state_loader.go b/pkg/gui/services/custom_commands/session_state_loader.go index 23369cb0b..81d30e5bf 100644 --- a/pkg/gui/services/custom_commands/session_state_loader.go +++ b/pkg/gui/services/custom_commands/session_state_loader.go @@ -71,8 +71,10 @@ func branchShimFromModelBranch(branch *models.Branch) *Branch { Name: branch.Name, DisplayName: branch.DisplayName, Recency: branch.Recency, - Pushables: branch.Pushables, - Pullables: branch.Pullables, + Pushables: branch.AheadForPull, + Pullables: branch.BehindForPull, + AheadForPull: branch.AheadForPull, + BehindForPull: branch.BehindForPull, UpstreamGone: branch.UpstreamGone, Head: branch.Head, DetachedHead: branch.DetachedHead, -- cgit v1.2.3 From d890c68cd0a77d3b5bf8bf1c4a672f8b0a0bfbb4 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 26 Apr 2024 11:19:53 +0200 Subject: Add ahead/behind information for @{push} In a triangular workflow the branch that you're pulling from is not the same as the one that you are pushing to. For example, some people find it useful to set the upstream branch to origin/master so that pulling effectively rebases onto master, and set the push.default git config to "current" so that "feature" pushes to origin/feature. Another example is a fork-based workflow where "feature" has upstream set to upstream/main, and the repo has remote.pushDefault set to "origin", so pushing on "feature" pushes to origin/feature. This commit adds new fields to models.Branch that store the ahead/behind information against the push branch; for the "normal" workflow where you pull and push from/to the upstream branch, AheadForPush/BehindForPush will be the same as AheadForPull/BehindForPull. --- pkg/commands/git.go | 2 +- pkg/commands/git_commands/branch_loader.go | 27 ++++++++++++++++------ pkg/commands/git_commands/branch_loader_test.go | 26 +++++++++++++++------ pkg/commands/models/branch.go | 4 ++++ pkg/gui/services/custom_commands/models.go | 2 ++ .../custom_commands/session_state_loader.go | 2 ++ 6 files changed, 48 insertions(+), 15 deletions(-) diff --git a/pkg/commands/git.go b/pkg/commands/git.go index b43c8c4e5..7e7d9354f 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -134,7 +134,7 @@ func NewGitCommandAux( worktreeCommands := git_commands.NewWorktreeCommands(gitCommon) blameCommands := git_commands.NewBlameCommands(gitCommon) - branchLoader := git_commands.NewBranchLoader(cmn, cmd, branchCommands.CurrentBranchInfo, configCommands) + branchLoader := git_commands.NewBranchLoader(cmn, gitCommon, cmd, branchCommands.CurrentBranchInfo, configCommands) commitFileLoader := git_commands.NewCommitFileLoader(cmn, cmd) commitLoader := git_commands.NewCommitLoader(cmn, cmd, statusCommands.RebaseMode, gitCommon) reflogCommitLoader := git_commands.NewReflogCommitLoader(cmn, cmd) diff --git a/pkg/commands/git_commands/branch_loader.go b/pkg/commands/git_commands/branch_loader.go index f1c68cd53..16777243a 100644 --- a/pkg/commands/git_commands/branch_loader.go +++ b/pkg/commands/git_commands/branch_loader.go @@ -40,6 +40,7 @@ type BranchInfo struct { // BranchLoader returns a list of Branch objects for the current repo type BranchLoader struct { *common.Common + *GitCommon cmd oscommands.ICmdObjBuilder getCurrentBranchInfo func() (BranchInfo, error) config BranchLoaderConfigCommands @@ -47,12 +48,14 @@ type BranchLoader struct { func NewBranchLoader( cmn *common.Common, + gitCommon *GitCommon, cmd oscommands.ICmdObjBuilder, getCurrentBranchInfo func() (BranchInfo, error), config BranchLoaderConfigCommands, ) *BranchLoader { return &BranchLoader{ Common: cmn, + GitCommon: gitCommon, cmd: cmd, getCurrentBranchInfo: getCurrentBranchInfo, config: config, @@ -61,7 +64,7 @@ func NewBranchLoader( // Load the list of branches for the current repo func (self *BranchLoader) Load(reflogCommits []*models.Commit) ([]*models.Branch, error) { - branches := self.obtainBranches() + branches := self.obtainBranches(self.version.IsAtLeast(2, 22, 0)) if self.AppState.LocalBranchSortOrder == "recency" { reflogBranches := self.obtainReflogBranches(reflogCommits) @@ -124,7 +127,7 @@ func (self *BranchLoader) Load(reflogCommits []*models.Commit) ([]*models.Branch return branches, nil } -func (self *BranchLoader) obtainBranches() []*models.Branch { +func (self *BranchLoader) obtainBranches(canUsePushTrack bool) []*models.Branch { output, err := self.getRawBranches() if err != nil { panic(err) @@ -147,7 +150,7 @@ func (self *BranchLoader) obtainBranches() []*models.Branch { } storeCommitDateAsRecency := self.AppState.LocalBranchSortOrder != "recency" - return obtainBranch(split, storeCommitDateAsRecency), true + return obtainBranch(split, storeCommitDateAsRecency, canUsePushTrack), true }) } @@ -183,23 +186,31 @@ var branchFields = []string{ "refname:short", "upstream:short", "upstream:track", + "push:track", "subject", "objectname", "committerdate:unix", } // Obtain branch information from parsed line output of getRawBranches() -func obtainBranch(split []string, storeCommitDateAsRecency bool) *models.Branch { +func obtainBranch(split []string, storeCommitDateAsRecency bool, canUsePushTrack bool) *models.Branch { headMarker := split[0] fullName := split[1] upstreamName := split[2] track := split[3] - subject := split[4] - commitHash := split[5] - commitDate := split[6] + pushTrack := split[4] + subject := split[5] + commitHash := split[6] + commitDate := split[7] name := strings.TrimPrefix(fullName, "heads/") aheadForPull, behindForPull, gone := parseUpstreamInfo(upstreamName, track) + var aheadForPush, behindForPush string + if canUsePushTrack { + aheadForPush, behindForPush, _ = parseUpstreamInfo(upstreamName, pushTrack) + } else { + aheadForPush, behindForPush = aheadForPull, behindForPull + } recency := "" if storeCommitDateAsRecency { @@ -213,6 +224,8 @@ func obtainBranch(split []string, storeCommitDateAsRecency bool) *models.Branch Recency: recency, AheadForPull: aheadForPull, BehindForPull: behindForPull, + AheadForPush: aheadForPush, + BehindForPush: behindForPush, UpstreamGone: gone, Head: headMarker == "*", Subject: subject, diff --git a/pkg/commands/git_commands/branch_loader_test.go b/pkg/commands/git_commands/branch_loader_test.go index e9b8001b5..2236374e5 100644 --- a/pkg/commands/git_commands/branch_loader_test.go +++ b/pkg/commands/git_commands/branch_loader_test.go @@ -25,12 +25,14 @@ func TestObtainBranch(t *testing.T) { scenarios := []scenario{ { testName: "TrimHeads", - input: []string{"", "heads/a_branch", "", "", "subject", "123", timeStamp}, + input: []string{"", "heads/a_branch", "", "", "", "subject", "123", timeStamp}, storeCommitDateAsRecency: false, expectedBranch: &models.Branch{ Name: "a_branch", AheadForPull: "?", BehindForPull: "?", + AheadForPush: "?", + BehindForPush: "?", Head: false, Subject: "subject", CommitHash: "123", @@ -38,12 +40,14 @@ func TestObtainBranch(t *testing.T) { }, { testName: "NoUpstream", - input: []string{"", "a_branch", "", "", "subject", "123", timeStamp}, + input: []string{"", "a_branch", "", "", "", "subject", "123", timeStamp}, storeCommitDateAsRecency: false, expectedBranch: &models.Branch{ Name: "a_branch", AheadForPull: "?", BehindForPull: "?", + AheadForPush: "?", + BehindForPush: "?", Head: false, Subject: "subject", CommitHash: "123", @@ -51,12 +55,14 @@ func TestObtainBranch(t *testing.T) { }, { testName: "IsHead", - input: []string{"*", "a_branch", "", "", "subject", "123", timeStamp}, + input: []string{"*", "a_branch", "", "", "", "subject", "123", timeStamp}, storeCommitDateAsRecency: false, expectedBranch: &models.Branch{ Name: "a_branch", AheadForPull: "?", BehindForPull: "?", + AheadForPush: "?", + BehindForPush: "?", Head: true, Subject: "subject", CommitHash: "123", @@ -64,12 +70,14 @@ func TestObtainBranch(t *testing.T) { }, { testName: "IsBehindAndAhead", - input: []string{"", "a_branch", "a_remote/a_branch", "[behind 2, ahead 3]", "subject", "123", timeStamp}, + input: []string{"", "a_branch", "a_remote/a_branch", "[behind 2, ahead 3]", "[behind 2, ahead 3]", "subject", "123", timeStamp}, storeCommitDateAsRecency: false, expectedBranch: &models.Branch{ Name: "a_branch", AheadForPull: "3", BehindForPull: "2", + AheadForPush: "3", + BehindForPush: "2", Head: false, Subject: "subject", CommitHash: "123", @@ -77,13 +85,15 @@ func TestObtainBranch(t *testing.T) { }, { testName: "RemoteBranchIsGone", - input: []string{"", "a_branch", "a_remote/a_branch", "[gone]", "subject", "123", timeStamp}, + input: []string{"", "a_branch", "a_remote/a_branch", "[gone]", "[gone]", "subject", "123", timeStamp}, storeCommitDateAsRecency: false, expectedBranch: &models.Branch{ Name: "a_branch", UpstreamGone: true, AheadForPull: "?", BehindForPull: "?", + AheadForPush: "?", + BehindForPush: "?", Head: false, Subject: "subject", CommitHash: "123", @@ -91,13 +101,15 @@ func TestObtainBranch(t *testing.T) { }, { testName: "WithCommitDateAsRecency", - input: []string{"", "a_branch", "", "", "subject", "123", timeStamp}, + input: []string{"", "a_branch", "", "", "", "subject", "123", timeStamp}, storeCommitDateAsRecency: true, expectedBranch: &models.Branch{ Name: "a_branch", Recency: "2h", AheadForPull: "?", BehindForPull: "?", + AheadForPush: "?", + BehindForPush: "?", Head: false, Subject: "subject", CommitHash: "123", @@ -107,7 +119,7 @@ func TestObtainBranch(t *testing.T) { for _, s := range scenarios { t.Run(s.testName, func(t *testing.T) { - branch := obtainBranch(s.input, s.storeCommitDateAsRecency) + branch := obtainBranch(s.input, s.storeCommitDateAsRecency, true) assert.EqualValues(t, s.expectedBranch, branch) }) } diff --git a/pkg/commands/models/branch.go b/pkg/commands/models/branch.go index 9e4772410..75d99f4bc 100644 --- a/pkg/commands/models/branch.go +++ b/pkg/commands/models/branch.go @@ -14,6 +14,10 @@ type Branch struct { AheadForPull string // how many commits behind we are from the remote branch (how many commits we can pull) BehindForPull string + // how many commits ahead we are from the branch we're pushing to (which might not be the same as our upstream branch in a triangular workflow) + AheadForPush string + // how many commits behind we are from the branch we're pushing to (which might not be the same as our upstream branch in a triangular workflow) + BehindForPush string // whether the remote branch is 'gone' i.e. we're tracking a remote branch that has been deleted UpstreamGone bool // whether this is the current branch. Exactly one branch should have this be true diff --git a/pkg/gui/services/custom_commands/models.go b/pkg/gui/services/custom_commands/models.go index bd3a69224..261bace45 100644 --- a/pkg/gui/services/custom_commands/models.go +++ b/pkg/gui/services/custom_commands/models.go @@ -51,6 +51,8 @@ type Branch struct { Pullables string // deprecated: use BehindForPull AheadForPull string BehindForPull string + AheadForPush string + BehindForPush string UpstreamGone bool Head bool DetachedHead bool diff --git a/pkg/gui/services/custom_commands/session_state_loader.go b/pkg/gui/services/custom_commands/session_state_loader.go index 81d30e5bf..6f39c5f8c 100644 --- a/pkg/gui/services/custom_commands/session_state_loader.go +++ b/pkg/gui/services/custom_commands/session_state_loader.go @@ -75,6 +75,8 @@ func branchShimFromModelBranch(branch *models.Branch) *Branch { Pullables: branch.BehindForPull, AheadForPull: branch.AheadForPull, BehindForPull: branch.BehindForPull, + AheadForPush: branch.AheadForPush, + BehindForPush: branch.BehindForPush, UpstreamGone: branch.UpstreamGone, Head: branch.Head, DetachedHead: branch.DetachedHead, -- cgit v1.2.3 From c5cf1b2428c7fd23f63a9fb7fe9a5911dd2f004a Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 26 Apr 2024 11:20:16 +0200 Subject: Correctly request force-pushing in triangular workflows To determine whether we need to ask for force pushing, we need to query the push branch rather than the upstream branch, in case they are not the same. --- pkg/commands/models/branch.go | 4 ++++ pkg/gui/controllers/sync_controller.go | 2 +- pkg/integration/tests/sync/force_push_triangular.go | 7 ------- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/pkg/commands/models/branch.go b/pkg/commands/models/branch.go index 75d99f4bc..25d806fca 100644 --- a/pkg/commands/models/branch.go +++ b/pkg/commands/models/branch.go @@ -103,6 +103,10 @@ func (b *Branch) IsBehindForPull() bool { return b.RemoteBranchStoredLocally() && b.BehindForPull != "0" } +func (b *Branch) IsBehindForPush() bool { + return b.BehindForPush != "" && b.BehindForPush != "0" +} + // for when we're in a detached head state func (b *Branch) IsRealBranch() bool { return b.AheadForPull != "" && b.BehindForPull != "" diff --git a/pkg/gui/controllers/sync_controller.go b/pkg/gui/controllers/sync_controller.go index 8bd8dac9a..7d7ca9eed 100644 --- a/pkg/gui/controllers/sync_controller.go +++ b/pkg/gui/controllers/sync_controller.go @@ -90,7 +90,7 @@ func (self *SyncController) push(currentBranch *models.Branch) error { // if we are behind our upstream branch we'll ask if the user wants to force push if currentBranch.IsTrackingRemote() { opts := pushOpts{} - if currentBranch.IsBehindForPull() { + if currentBranch.IsBehindForPush() { return self.requestToForcePush(currentBranch, opts) } else { return self.pushAux(currentBranch, opts) diff --git a/pkg/integration/tests/sync/force_push_triangular.go b/pkg/integration/tests/sync/force_push_triangular.go index 379912f6c..70912d123 100644 --- a/pkg/integration/tests/sync/force_push_triangular.go +++ b/pkg/integration/tests/sync/force_push_triangular.go @@ -36,12 +36,6 @@ var ForcePushTriangular = NewIntegrationTest(NewIntegrationTestArgs{ t.Views().Files().IsFocused().Press(keys.Universal.Push) - // This results in an attempt to push normally, which fails with an error: - t.ExpectPopup().Alert(). - Title(Equals("Error")). - Content(Contains("Updates were rejected. Please fetch and examine the remote changes before pushing again.")) - - /* EXPECTED: t.ExpectPopup().Confirmation(). Title(Equals("Force push")). Content(Equals("Your branch has diverged from the remote branch. Press to cancel, or to force push.")). @@ -67,6 +61,5 @@ var ForcePushTriangular = NewIntegrationTest(NewIntegrationTestArgs{ t.Views().SubCommits().IsFocused(). Lines(Contains("one")) - */ }, }) -- cgit v1.2.3