diff --git a/docs/Custom_Command_Keybindings.md b/docs/Custom_Command_Keybindings.md index 8604a5247d6..426d6f8f699 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/commands/git.go b/pkg/commands/git.go index b43c8c4e55a..7e7d9354fe7 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 198368502b6..16777243a86 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/") - pushables, pullables, gone := parseUpstreamInfo(upstreamName, track) + 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 { @@ -209,14 +220,16 @@ 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, + AheadForPush: aheadForPush, + BehindForPush: behindForPush, + UpstreamGone: gone, + Head: headMarker == "*", + Subject: subject, + CommitHash: commitHash, } } @@ -232,10 +245,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 9e56666fee7..2236374e510 100644 --- a/pkg/commands/git_commands/branch_loader_test.go +++ b/pkg/commands/git_commands/branch_loader_test.go @@ -25,89 +25,101 @@ 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", - Pushables: "?", - Pullables: "?", - Head: false, - Subject: "subject", - CommitHash: "123", + Name: "a_branch", + AheadForPull: "?", + BehindForPull: "?", + AheadForPush: "?", + BehindForPush: "?", + Head: false, + Subject: "subject", + CommitHash: "123", }, }, { testName: "NoUpstream", - input: []string{"", "a_branch", "", "", "subject", "123", timeStamp}, + 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: "?", + AheadForPush: "?", + BehindForPush: "?", + Head: false, + Subject: "subject", + CommitHash: "123", }, }, { testName: "IsHead", - input: []string{"*", "a_branch", "", "", "subject", "123", timeStamp}, + 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: "?", + AheadForPush: "?", + BehindForPush: "?", + Head: true, + Subject: "subject", + CommitHash: "123", }, }, { 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", - Pushables: "3", - Pullables: "2", - Head: false, - Subject: "subject", - CommitHash: "123", + Name: "a_branch", + AheadForPull: "3", + BehindForPull: "2", + AheadForPush: "3", + BehindForPush: "2", + Head: false, + Subject: "subject", + CommitHash: "123", }, }, { 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, - Pushables: "?", - Pullables: "?", - Head: false, - Subject: "subject", - CommitHash: "123", + Name: "a_branch", + UpstreamGone: true, + AheadForPull: "?", + BehindForPull: "?", + AheadForPush: "?", + BehindForPush: "?", + Head: false, + Subject: "subject", + CommitHash: "123", }, }, { 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", - Pushables: "?", - Pullables: "?", - Head: false, - Subject: "subject", - CommitHash: "123", + Name: "a_branch", + Recency: "2h", + AheadForPull: "?", + BehindForPull: "?", + AheadForPush: "?", + BehindForPush: "?", + Head: false, + Subject: "subject", + CommitHash: "123", }, }, } 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 c5fcfdaedbd..25d806fca0e 100644 --- a/pkg/commands/models/branch.go +++ b/pkg/commands/models/branch.go @@ -10,10 +10,14 @@ 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 + // 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 @@ -80,26 +84,30 @@ 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" +} + +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.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 b08ddd0cd76..d7faa78118d 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 403f31d94e2..7d7ca9eed7d 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.IsBehindForPush() { 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 0abf2d4cd08..406a580d5fd 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 250b143e320..cf2f1d994f8 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 new file mode 100644 index 00000000000..261bace45ac --- /dev/null +++ b/pkg/gui/services/custom_commands/models.go @@ -0,0 +1,100 @@ +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 // deprecated: use AheadForPull + Pullables string // deprecated: use BehindForPull + AheadForPull string + BehindForPull string + AheadForPush string + BehindForPush 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 6a3068df8e6..6f39c5f8cd2 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,160 @@ 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.AheadForPull, + Pullables: branch.BehindForPull, + AheadForPull: branch.AheadForPull, + BehindForPull: branch.BehindForPull, + AheadForPush: branch.AheadForPush, + BehindForPush: branch.BehindForPush, + 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()), } } diff --git a/pkg/integration/components/shell.go b/pkg/integration/components/shell.go index e3df61a50e3..a8caff77d5f 100644 --- a/pkg/integration/components/shell.go +++ b/pkg/integration/components/shell.go @@ -195,6 +195,10 @@ func (self *Shell) CreateAnnotatedTag(name string, message string, ref string) * } 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/branch/delete.go b/pkg/integration/tests/branch/delete.go index 0b6adfac4d0..f81eb060970 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 f145eceaaf1..ad8f70c435c 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 5c9e0f388cf..2469eb012d4 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 c933787e492..3cdbb561d80 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"). 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 00000000000..70912d1232c --- /dev/null +++ b/pkg/integration/tests/sync/force_push_triangular.go @@ -0,0 +1,65 @@ +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) + + 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 7084fd99b3c..e043d4a8a21 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,