1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-07-03 00:57:52 +02:00

Correctly request force-pushing in a triangular workflow (#3528)

- **PR Description**

Some people push to a different branch (or even remote) than they pull
from. One example is described in #3437. Our logic of when to request a
force push is not appropriate for these workflows: we check the
configured upstream branch for divergence, but that's the one you pull
from. We should instead check the push-to branch for divergence.

Fixes #3437.
This commit is contained in:
Stefan Haller
2024-05-19 10:00:32 +02:00
committed by GitHub
18 changed files with 449 additions and 140 deletions

View File

@ -305,7 +305,7 @@ SelectedWorktree
CheckedOutBranch 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 ## Keybinding collisions

View File

@ -134,7 +134,7 @@ func NewGitCommandAux(
worktreeCommands := git_commands.NewWorktreeCommands(gitCommon) worktreeCommands := git_commands.NewWorktreeCommands(gitCommon)
blameCommands := git_commands.NewBlameCommands(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) commitFileLoader := git_commands.NewCommitFileLoader(cmn, cmd)
commitLoader := git_commands.NewCommitLoader(cmn, cmd, statusCommands.RebaseMode, gitCommon) commitLoader := git_commands.NewCommitLoader(cmn, cmd, statusCommands.RebaseMode, gitCommon)
reflogCommitLoader := git_commands.NewReflogCommitLoader(cmn, cmd) reflogCommitLoader := git_commands.NewReflogCommitLoader(cmn, cmd)

View File

@ -40,6 +40,7 @@ type BranchInfo struct {
// BranchLoader returns a list of Branch objects for the current repo // BranchLoader returns a list of Branch objects for the current repo
type BranchLoader struct { type BranchLoader struct {
*common.Common *common.Common
*GitCommon
cmd oscommands.ICmdObjBuilder cmd oscommands.ICmdObjBuilder
getCurrentBranchInfo func() (BranchInfo, error) getCurrentBranchInfo func() (BranchInfo, error)
config BranchLoaderConfigCommands config BranchLoaderConfigCommands
@ -47,12 +48,14 @@ type BranchLoader struct {
func NewBranchLoader( func NewBranchLoader(
cmn *common.Common, cmn *common.Common,
gitCommon *GitCommon,
cmd oscommands.ICmdObjBuilder, cmd oscommands.ICmdObjBuilder,
getCurrentBranchInfo func() (BranchInfo, error), getCurrentBranchInfo func() (BranchInfo, error),
config BranchLoaderConfigCommands, config BranchLoaderConfigCommands,
) *BranchLoader { ) *BranchLoader {
return &BranchLoader{ return &BranchLoader{
Common: cmn, Common: cmn,
GitCommon: gitCommon,
cmd: cmd, cmd: cmd,
getCurrentBranchInfo: getCurrentBranchInfo, getCurrentBranchInfo: getCurrentBranchInfo,
config: config, config: config,
@ -61,7 +64,7 @@ func NewBranchLoader(
// Load the list of branches for the current repo // Load the list of branches for the current repo
func (self *BranchLoader) Load(reflogCommits []*models.Commit) ([]*models.Branch, error) { 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" { if self.AppState.LocalBranchSortOrder == "recency" {
reflogBranches := self.obtainReflogBranches(reflogCommits) reflogBranches := self.obtainReflogBranches(reflogCommits)
@ -124,7 +127,7 @@ func (self *BranchLoader) Load(reflogCommits []*models.Commit) ([]*models.Branch
return branches, nil return branches, nil
} }
func (self *BranchLoader) obtainBranches() []*models.Branch { func (self *BranchLoader) obtainBranches(canUsePushTrack bool) []*models.Branch {
output, err := self.getRawBranches() output, err := self.getRawBranches()
if err != nil { if err != nil {
panic(err) panic(err)
@ -147,7 +150,7 @@ func (self *BranchLoader) obtainBranches() []*models.Branch {
} }
storeCommitDateAsRecency := self.AppState.LocalBranchSortOrder != "recency" 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", "refname:short",
"upstream:short", "upstream:short",
"upstream:track", "upstream:track",
"push:track",
"subject", "subject",
"objectname", "objectname",
"committerdate:unix", "committerdate:unix",
} }
// Obtain branch information from parsed line output of getRawBranches() // 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] headMarker := split[0]
fullName := split[1] fullName := split[1]
upstreamName := split[2] upstreamName := split[2]
track := split[3] track := split[3]
subject := split[4] pushTrack := split[4]
commitHash := split[5] subject := split[5]
commitDate := split[6] commitHash := split[6]
commitDate := split[7]
name := strings.TrimPrefix(fullName, "heads/") 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 := "" recency := ""
if storeCommitDateAsRecency { if storeCommitDateAsRecency {
@ -209,14 +220,16 @@ func obtainBranch(split []string, storeCommitDateAsRecency bool) *models.Branch
} }
return &models.Branch{ return &models.Branch{
Name: name, Name: name,
Recency: recency, Recency: recency,
Pushables: pushables, AheadForPull: aheadForPull,
Pullables: pullables, BehindForPull: behindForPull,
UpstreamGone: gone, AheadForPush: aheadForPush,
Head: headMarker == "*", BehindForPush: behindForPush,
Subject: subject, UpstreamGone: gone,
CommitHash: commitHash, Head: headMarker == "*",
Subject: subject,
CommitHash: commitHash,
} }
} }
@ -232,10 +245,10 @@ func parseUpstreamInfo(upstreamName string, track string) (string, string, bool)
return "?", "?", true return "?", "?", true
} }
pushables := parseDifference(track, `ahead (\d+)`) ahead := parseDifference(track, `ahead (\d+)`)
pullables := parseDifference(track, `behind (\d+)`) behind := parseDifference(track, `behind (\d+)`)
return pushables, pullables, false return ahead, behind, false
} }
func parseDifference(track string, regexStr string) string { func parseDifference(track string, regexStr string) string {

View File

@ -25,89 +25,101 @@ func TestObtainBranch(t *testing.T) {
scenarios := []scenario{ scenarios := []scenario{
{ {
testName: "TrimHeads", testName: "TrimHeads",
input: []string{"", "heads/a_branch", "", "", "subject", "123", timeStamp}, input: []string{"", "heads/a_branch", "", "", "", "subject", "123", timeStamp},
storeCommitDateAsRecency: false, storeCommitDateAsRecency: false,
expectedBranch: &models.Branch{ expectedBranch: &models.Branch{
Name: "a_branch", Name: "a_branch",
Pushables: "?", AheadForPull: "?",
Pullables: "?", BehindForPull: "?",
Head: false, AheadForPush: "?",
Subject: "subject", BehindForPush: "?",
CommitHash: "123", Head: false,
Subject: "subject",
CommitHash: "123",
}, },
}, },
{ {
testName: "NoUpstream", testName: "NoUpstream",
input: []string{"", "a_branch", "", "", "subject", "123", timeStamp}, input: []string{"", "a_branch", "", "", "", "subject", "123", timeStamp},
storeCommitDateAsRecency: false, storeCommitDateAsRecency: false,
expectedBranch: &models.Branch{ expectedBranch: &models.Branch{
Name: "a_branch", Name: "a_branch",
Pushables: "?", AheadForPull: "?",
Pullables: "?", BehindForPull: "?",
Head: false, AheadForPush: "?",
Subject: "subject", BehindForPush: "?",
CommitHash: "123", Head: false,
Subject: "subject",
CommitHash: "123",
}, },
}, },
{ {
testName: "IsHead", testName: "IsHead",
input: []string{"*", "a_branch", "", "", "subject", "123", timeStamp}, input: []string{"*", "a_branch", "", "", "", "subject", "123", timeStamp},
storeCommitDateAsRecency: false, storeCommitDateAsRecency: false,
expectedBranch: &models.Branch{ expectedBranch: &models.Branch{
Name: "a_branch", Name: "a_branch",
Pushables: "?", AheadForPull: "?",
Pullables: "?", BehindForPull: "?",
Head: true, AheadForPush: "?",
Subject: "subject", BehindForPush: "?",
CommitHash: "123", Head: true,
Subject: "subject",
CommitHash: "123",
}, },
}, },
{ {
testName: "IsBehindAndAhead", 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, storeCommitDateAsRecency: false,
expectedBranch: &models.Branch{ expectedBranch: &models.Branch{
Name: "a_branch", Name: "a_branch",
Pushables: "3", AheadForPull: "3",
Pullables: "2", BehindForPull: "2",
Head: false, AheadForPush: "3",
Subject: "subject", BehindForPush: "2",
CommitHash: "123", Head: false,
Subject: "subject",
CommitHash: "123",
}, },
}, },
{ {
testName: "RemoteBranchIsGone", 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, storeCommitDateAsRecency: false,
expectedBranch: &models.Branch{ expectedBranch: &models.Branch{
Name: "a_branch", Name: "a_branch",
UpstreamGone: true, UpstreamGone: true,
Pushables: "?", AheadForPull: "?",
Pullables: "?", BehindForPull: "?",
Head: false, AheadForPush: "?",
Subject: "subject", BehindForPush: "?",
CommitHash: "123", Head: false,
Subject: "subject",
CommitHash: "123",
}, },
}, },
{ {
testName: "WithCommitDateAsRecency", testName: "WithCommitDateAsRecency",
input: []string{"", "a_branch", "", "", "subject", "123", timeStamp}, input: []string{"", "a_branch", "", "", "", "subject", "123", timeStamp},
storeCommitDateAsRecency: true, storeCommitDateAsRecency: true,
expectedBranch: &models.Branch{ expectedBranch: &models.Branch{
Name: "a_branch", Name: "a_branch",
Recency: "2h", Recency: "2h",
Pushables: "?", AheadForPull: "?",
Pullables: "?", BehindForPull: "?",
Head: false, AheadForPush: "?",
Subject: "subject", BehindForPush: "?",
CommitHash: "123", Head: false,
Subject: "subject",
CommitHash: "123",
}, },
}, },
} }
for _, s := range scenarios { for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) { 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) assert.EqualValues(t, s.expectedBranch, branch)
}) })
} }

View File

@ -10,10 +10,14 @@ type Branch struct {
DisplayName string DisplayName string
// indicator of when the branch was last checked out e.g. '2d', '3m' // indicator of when the branch was last checked out e.g. '2d', '3m'
Recency string Recency string
// how many commits ahead we are from the remote branch (how many commits we can push) // how many commits ahead we are from the remote branch (how many commits we can push, assuming we push to our tracked remote branch)
Pushables string AheadForPull string
// how many commits behind we are from the remote branch (how many commits we can pull) // 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 // whether the remote branch is 'gone' i.e. we're tracking a remote branch that has been deleted
UpstreamGone bool UpstreamGone bool
// whether this is the current branch. Exactly one branch should have this be true // 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 // we know that the remote branch is not stored locally based on our pushable/pullable
// count being question marks. // count being question marks.
func (b *Branch) RemoteBranchStoredLocally() bool { func (b *Branch) RemoteBranchStoredLocally() bool {
return b.IsTrackingRemote() && b.Pushables != "?" && b.Pullables != "?" return b.IsTrackingRemote() && b.AheadForPull != "?" && b.BehindForPull != "?"
} }
func (b *Branch) RemoteBranchNotStoredLocally() bool { func (b *Branch) RemoteBranchNotStoredLocally() bool {
return b.IsTrackingRemote() && b.Pushables == "?" && b.Pullables == "?" return b.IsTrackingRemote() && b.AheadForPull == "?" && b.BehindForPull == "?"
} }
func (b *Branch) MatchesUpstream() bool { 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 { func (b *Branch) IsAheadForPull() bool {
return b.RemoteBranchStoredLocally() && b.Pushables != "0" return b.RemoteBranchStoredLocally() && b.AheadForPull != "0"
} }
func (b *Branch) HasCommitsToPull() bool { func (b *Branch) IsBehindForPull() bool {
return b.RemoteBranchStoredLocally() && b.Pullables != "0" 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 // for when we're in a detached head state
func (b *Branch) IsRealBranch() bool { func (b *Branch) IsRealBranch() bool {
return b.Pushables != "" && b.Pullables != "" return b.AheadForPull != "" && b.BehindForPull != ""
} }

View File

@ -620,7 +620,7 @@ func (self *BranchesController) fastForward(branch *models.Branch) error {
if !branch.RemoteBranchStoredLocally() { if !branch.RemoteBranchStoredLocally() {
return errors.New(self.c.Tr.FwdNoLocalUpstream) return errors.New(self.c.Tr.FwdNoLocalUpstream)
} }
if branch.HasCommitsToPush() { if branch.IsAheadForPull() {
return errors.New(self.c.Tr.FwdCommitsToPush) return errors.New(self.c.Tr.FwdCommitsToPush)
} }

View File

@ -87,10 +87,10 @@ func (self *SyncController) branchCheckedOut(f func(*models.Branch) error) func(
} }
func (self *SyncController) push(currentBranch *models.Branch) error { 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() { if currentBranch.IsTrackingRemote() {
opts := pushOpts{} opts := pushOpts{}
if currentBranch.HasCommitsToPull() { if currentBranch.IsBehindForPush() {
return self.requestToForcePush(currentBranch, opts) return self.requestToForcePush(currentBranch, opts)
} else { } else {
return self.pushAux(currentBranch, opts) return self.pushAux(currentBranch, opts)

View File

@ -196,11 +196,11 @@ func BranchStatus(
} }
result := "" result := ""
if branch.HasCommitsToPush() { if branch.IsAheadForPull() {
result = fmt.Sprintf("↑%s", branch.Pushables) result = fmt.Sprintf("↑%s", branch.AheadForPull)
} }
if branch.HasCommitsToPull() { if branch.IsBehindForPull() {
result = fmt.Sprintf("%s↓%s", result, branch.Pullables) result = fmt.Sprintf("%s↓%s", result, branch.BehindForPull)
} }
return result return result

View File

@ -58,8 +58,8 @@ func Test_getBranchDisplayStrings(t *testing.T) {
Name: "branch_name", Name: "branch_name",
Recency: "1m", Recency: "1m",
UpstreamRemote: "origin", UpstreamRemote: "origin",
Pushables: "0", AheadForPull: "0",
Pullables: "0", BehindForPull: "0",
}, },
itemOperation: types.ItemOperationNone, itemOperation: types.ItemOperationNone,
fullDescription: false, fullDescription: false,
@ -73,8 +73,8 @@ func Test_getBranchDisplayStrings(t *testing.T) {
Name: "branch_name", Name: "branch_name",
Recency: "1m", Recency: "1m",
UpstreamRemote: "origin", UpstreamRemote: "origin",
Pushables: "3", AheadForPull: "3",
Pullables: "5", BehindForPull: "5",
}, },
itemOperation: types.ItemOperationNone, itemOperation: types.ItemOperationNone,
fullDescription: false, fullDescription: false,
@ -99,8 +99,8 @@ func Test_getBranchDisplayStrings(t *testing.T) {
CommitHash: "1234567890", CommitHash: "1234567890",
UpstreamRemote: "origin", UpstreamRemote: "origin",
UpstreamBranch: "branch_name", UpstreamBranch: "branch_name",
Pushables: "0", AheadForPull: "0",
Pullables: "0", BehindForPull: "0",
Subject: "commit title", Subject: "commit title",
}, },
itemOperation: types.ItemOperationNone, itemOperation: types.ItemOperationNone,
@ -144,8 +144,8 @@ func Test_getBranchDisplayStrings(t *testing.T) {
Name: "branch_name", Name: "branch_name",
Recency: "1m", Recency: "1m",
UpstreamRemote: "origin", UpstreamRemote: "origin",
Pushables: "0", AheadForPull: "0",
Pullables: "0", BehindForPull: "0",
}, },
itemOperation: types.ItemOperationNone, itemOperation: types.ItemOperationNone,
fullDescription: false, fullDescription: false,
@ -159,8 +159,8 @@ func Test_getBranchDisplayStrings(t *testing.T) {
Name: "branch_name", Name: "branch_name",
Recency: "1m", Recency: "1m",
UpstreamRemote: "origin", UpstreamRemote: "origin",
Pushables: "3", AheadForPull: "3",
Pullables: "5", BehindForPull: "5",
}, },
itemOperation: types.ItemOperationNone, itemOperation: types.ItemOperationNone,
fullDescription: false, fullDescription: false,
@ -212,8 +212,8 @@ func Test_getBranchDisplayStrings(t *testing.T) {
CommitHash: "1234567890", CommitHash: "1234567890",
UpstreamRemote: "origin", UpstreamRemote: "origin",
UpstreamBranch: "branch_name", UpstreamBranch: "branch_name",
Pushables: "0", AheadForPull: "0",
Pullables: "0", BehindForPull: "0",
Subject: "commit title", Subject: "commit title",
}, },
itemOperation: types.ItemOperationNone, itemOperation: types.ItemOperationNone,

View File

@ -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
}

View File

@ -3,7 +3,7 @@ package custom_commands
import ( import (
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" "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 // 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 { func commitShimFromModelCommit(commit *models.Commit) *Commit {
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 {
if commit == nil { if commit == nil {
return 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 // SessionState captures the current state of the application for use in custom commands
type SessionState struct { type SessionState struct {
SelectedLocalCommit *Commit SelectedLocalCommit *Commit
SelectedReflogCommit *Commit SelectedReflogCommit *Commit
SelectedSubCommit *Commit SelectedSubCommit *Commit
SelectedFile *models.File SelectedFile *File
SelectedPath string SelectedPath string
SelectedLocalBranch *models.Branch SelectedLocalBranch *Branch
SelectedRemoteBranch *models.RemoteBranch SelectedRemoteBranch *RemoteBranch
SelectedRemote *models.Remote SelectedRemote *Remote
SelectedTag *models.Tag SelectedTag *Tag
SelectedStashEntry *models.StashEntry SelectedStashEntry *StashEntry
SelectedCommitFile *models.CommitFile SelectedCommitFile *CommitFile
SelectedCommitFilePath string SelectedCommitFilePath string
SelectedWorktree *models.Worktree SelectedWorktree *Worktree
CheckedOutBranch *models.Branch CheckedOutBranch *Branch
} }
func (self *SessionStateLoader) call() *SessionState { func (self *SessionStateLoader) call() *SessionState {
return &SessionState{ return &SessionState{
SelectedFile: self.c.Contexts().Files.GetSelectedFile(), SelectedFile: fileShimFromModelFile(self.c.Contexts().Files.GetSelectedFile()),
SelectedPath: self.c.Contexts().Files.GetSelectedPath(), SelectedPath: self.c.Contexts().Files.GetSelectedPath(),
SelectedLocalCommit: commitWrapperFromModelCommit(self.c.Contexts().LocalCommits.GetSelected()), SelectedLocalCommit: commitShimFromModelCommit(self.c.Contexts().LocalCommits.GetSelected()),
SelectedReflogCommit: commitWrapperFromModelCommit(self.c.Contexts().ReflogCommits.GetSelected()), SelectedReflogCommit: commitShimFromModelCommit(self.c.Contexts().ReflogCommits.GetSelected()),
SelectedLocalBranch: self.c.Contexts().Branches.GetSelected(), SelectedLocalBranch: branchShimFromModelBranch(self.c.Contexts().Branches.GetSelected()),
SelectedRemoteBranch: self.c.Contexts().RemoteBranches.GetSelected(), SelectedRemoteBranch: remoteBranchShimFromModelRemoteBranch(self.c.Contexts().RemoteBranches.GetSelected()),
SelectedRemote: self.c.Contexts().Remotes.GetSelected(), SelectedRemote: remoteShimFromModelRemote(self.c.Contexts().Remotes.GetSelected()),
SelectedTag: self.c.Contexts().Tags.GetSelected(), SelectedTag: tagShimFromModelRemote(self.c.Contexts().Tags.GetSelected()),
SelectedStashEntry: self.c.Contexts().Stash.GetSelected(), SelectedStashEntry: stashEntryShimFromModelRemote(self.c.Contexts().Stash.GetSelected()),
SelectedCommitFile: self.c.Contexts().CommitFiles.GetSelectedFile(), SelectedCommitFile: commitFileShimFromModelRemote(self.c.Contexts().CommitFiles.GetSelectedFile()),
SelectedCommitFilePath: self.c.Contexts().CommitFiles.GetSelectedPath(), SelectedCommitFilePath: self.c.Contexts().CommitFiles.GetSelectedPath(),
SelectedSubCommit: commitWrapperFromModelCommit(self.c.Contexts().SubCommits.GetSelected()), SelectedSubCommit: commitShimFromModelCommit(self.c.Contexts().SubCommits.GetSelected()),
SelectedWorktree: self.c.Contexts().Worktrees.GetSelected(), SelectedWorktree: worktreeShimFromModelRemote(self.c.Contexts().Worktrees.GetSelected()),
CheckedOutBranch: self.refsHelper.GetCheckedOutRef(), CheckedOutBranch: branchShimFromModelBranch(self.refsHelper.GetCheckedOutRef()),
} }
} }

View File

@ -195,6 +195,10 @@ func (self *Shell) CreateAnnotatedTag(name string, message string, ref string) *
} }
func (self *Shell) PushBranch(upstream, branch string) *Shell { 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}) return self.RunCommand([]string{"git", "push", "--set-upstream", upstream, branch})
} }

View File

@ -15,9 +15,9 @@ var Delete = NewIntegrationTest(NewIntegrationTestArgs{
CloneIntoRemote("origin"). CloneIntoRemote("origin").
EmptyCommit("blah"). EmptyCommit("blah").
NewBranch("branch-one"). NewBranch("branch-one").
PushBranch("origin", "branch-one"). PushBranchAndSetUpstream("origin", "branch-one").
NewBranch("branch-two"). NewBranch("branch-two").
PushBranch("origin", "branch-two"). PushBranchAndSetUpstream("origin", "branch-two").
EmptyCommit("deletion blocker"). EmptyCommit("deletion blocker").
NewBranch("branch-three") NewBranch("branch-three")
}, },

View File

@ -18,7 +18,7 @@ var DeleteRemoteBranchWithCredentialPrompt = NewIntegrationTest(NewIntegrationTe
shell.NewBranch("mybranch") 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. // 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. // This is not easy to do in a cross-platform way, nor is it easy to do in a docker container.

View File

@ -15,7 +15,7 @@ var RebaseToUpstream = NewIntegrationTest(NewIntegrationTestArgs{
CloneIntoRemote("origin"). CloneIntoRemote("origin").
EmptyCommit("ensure-master"). EmptyCommit("ensure-master").
EmptyCommit("to-be-added"). // <- this will only exist remotely EmptyCommit("to-be-added"). // <- this will only exist remotely
PushBranch("origin", "master"). PushBranchAndSetUpstream("origin", "master").
HardReset("HEAD~1"). HardReset("HEAD~1").
NewBranchFrom("base-branch", "master"). NewBranchFrom("base-branch", "master").
EmptyCommit("base-branch-commit"). EmptyCommit("base-branch-commit").

View File

@ -15,10 +15,10 @@ var ResetToUpstream = NewIntegrationTest(NewIntegrationTestArgs{
CloneIntoRemote("origin"). CloneIntoRemote("origin").
NewBranch("hard-branch"). NewBranch("hard-branch").
EmptyCommit("hard commit"). EmptyCommit("hard commit").
PushBranch("origin", "hard-branch"). PushBranchAndSetUpstream("origin", "hard-branch").
NewBranch("soft-branch"). NewBranch("soft-branch").
EmptyCommit("soft commit"). EmptyCommit("soft commit").
PushBranch("origin", "soft-branch"). PushBranchAndSetUpstream("origin", "soft-branch").
NewBranch("base"). NewBranch("base").
EmptyCommit("base-branch commit"). EmptyCommit("base-branch commit").
CreateFile("file-1", "content"). CreateFile("file-1", "content").

View File

@ -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 <esc> to cancel, or <enter> 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"))
},
})

View File

@ -275,6 +275,7 @@ var tests = []*components.IntegrationTest{
sync.ForcePush, sync.ForcePush,
sync.ForcePushMultipleMatching, sync.ForcePushMultipleMatching,
sync.ForcePushMultipleUpstream, sync.ForcePushMultipleUpstream,
sync.ForcePushTriangular,
sync.Pull, sync.Pull,
sync.PullAndSetUpstream, sync.PullAndSetUpstream,
sync.PullMerge, sync.PullMerge,