Skip to content

Commit 6fcb7eb

Browse files
authored
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.
2 parents 9fc7a51 + c5cf1b2 commit 6fcb7eb

18 files changed

+449
-140
lines changed

docs/Custom_Command_Keybindings.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ SelectedWorktree
305305
CheckedOutBranch
306306
```
307307

308-
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.
308+
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).
309309

310310
## Keybinding collisions
311311

pkg/commands/git.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ func NewGitCommandAux(
134134
worktreeCommands := git_commands.NewWorktreeCommands(gitCommon)
135135
blameCommands := git_commands.NewBlameCommands(gitCommon)
136136

137-
branchLoader := git_commands.NewBranchLoader(cmn, cmd, branchCommands.CurrentBranchInfo, configCommands)
137+
branchLoader := git_commands.NewBranchLoader(cmn, gitCommon, cmd, branchCommands.CurrentBranchInfo, configCommands)
138138
commitFileLoader := git_commands.NewCommitFileLoader(cmn, cmd)
139139
commitLoader := git_commands.NewCommitLoader(cmn, cmd, statusCommands.RebaseMode, gitCommon)
140140
reflogCommitLoader := git_commands.NewReflogCommitLoader(cmn, cmd)

pkg/commands/git_commands/branch_loader.go

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,22 @@ type BranchInfo struct {
4040
// BranchLoader returns a list of Branch objects for the current repo
4141
type BranchLoader struct {
4242
*common.Common
43+
*GitCommon
4344
cmd oscommands.ICmdObjBuilder
4445
getCurrentBranchInfo func() (BranchInfo, error)
4546
config BranchLoaderConfigCommands
4647
}
4748

4849
func NewBranchLoader(
4950
cmn *common.Common,
51+
gitCommon *GitCommon,
5052
cmd oscommands.ICmdObjBuilder,
5153
getCurrentBranchInfo func() (BranchInfo, error),
5254
config BranchLoaderConfigCommands,
5355
) *BranchLoader {
5456
return &BranchLoader{
5557
Common: cmn,
58+
GitCommon: gitCommon,
5659
cmd: cmd,
5760
getCurrentBranchInfo: getCurrentBranchInfo,
5861
config: config,
@@ -61,7 +64,7 @@ func NewBranchLoader(
6164

6265
// Load the list of branches for the current repo
6366
func (self *BranchLoader) Load(reflogCommits []*models.Commit) ([]*models.Branch, error) {
64-
branches := self.obtainBranches()
67+
branches := self.obtainBranches(self.version.IsAtLeast(2, 22, 0))
6568

6669
if self.AppState.LocalBranchSortOrder == "recency" {
6770
reflogBranches := self.obtainReflogBranches(reflogCommits)
@@ -124,7 +127,7 @@ func (self *BranchLoader) Load(reflogCommits []*models.Commit) ([]*models.Branch
124127
return branches, nil
125128
}
126129

127-
func (self *BranchLoader) obtainBranches() []*models.Branch {
130+
func (self *BranchLoader) obtainBranches(canUsePushTrack bool) []*models.Branch {
128131
output, err := self.getRawBranches()
129132
if err != nil {
130133
panic(err)
@@ -147,7 +150,7 @@ func (self *BranchLoader) obtainBranches() []*models.Branch {
147150
}
148151

149152
storeCommitDateAsRecency := self.AppState.LocalBranchSortOrder != "recency"
150-
return obtainBranch(split, storeCommitDateAsRecency), true
153+
return obtainBranch(split, storeCommitDateAsRecency, canUsePushTrack), true
151154
})
152155
}
153156

@@ -183,23 +186,31 @@ var branchFields = []string{
183186
"refname:short",
184187
"upstream:short",
185188
"upstream:track",
189+
"push:track",
186190
"subject",
187191
"objectname",
188192
"committerdate:unix",
189193
}
190194

191195
// Obtain branch information from parsed line output of getRawBranches()
192-
func obtainBranch(split []string, storeCommitDateAsRecency bool) *models.Branch {
196+
func obtainBranch(split []string, storeCommitDateAsRecency bool, canUsePushTrack bool) *models.Branch {
193197
headMarker := split[0]
194198
fullName := split[1]
195199
upstreamName := split[2]
196200
track := split[3]
197-
subject := split[4]
198-
commitHash := split[5]
199-
commitDate := split[6]
201+
pushTrack := split[4]
202+
subject := split[5]
203+
commitHash := split[6]
204+
commitDate := split[7]
200205

201206
name := strings.TrimPrefix(fullName, "heads/")
202-
pushables, pullables, gone := parseUpstreamInfo(upstreamName, track)
207+
aheadForPull, behindForPull, gone := parseUpstreamInfo(upstreamName, track)
208+
var aheadForPush, behindForPush string
209+
if canUsePushTrack {
210+
aheadForPush, behindForPush, _ = parseUpstreamInfo(upstreamName, pushTrack)
211+
} else {
212+
aheadForPush, behindForPush = aheadForPull, behindForPull
213+
}
203214

204215
recency := ""
205216
if storeCommitDateAsRecency {
@@ -209,14 +220,16 @@ func obtainBranch(split []string, storeCommitDateAsRecency bool) *models.Branch
209220
}
210221

211222
return &models.Branch{
212-
Name: name,
213-
Recency: recency,
214-
Pushables: pushables,
215-
Pullables: pullables,
216-
UpstreamGone: gone,
217-
Head: headMarker == "*",
218-
Subject: subject,
219-
CommitHash: commitHash,
223+
Name: name,
224+
Recency: recency,
225+
AheadForPull: aheadForPull,
226+
BehindForPull: behindForPull,
227+
AheadForPush: aheadForPush,
228+
BehindForPush: behindForPush,
229+
UpstreamGone: gone,
230+
Head: headMarker == "*",
231+
Subject: subject,
232+
CommitHash: commitHash,
220233
}
221234
}
222235

@@ -232,10 +245,10 @@ func parseUpstreamInfo(upstreamName string, track string) (string, string, bool)
232245
return "?", "?", true
233246
}
234247

235-
pushables := parseDifference(track, `ahead (\d+)`)
236-
pullables := parseDifference(track, `behind (\d+)`)
248+
ahead := parseDifference(track, `ahead (\d+)`)
249+
behind := parseDifference(track, `behind (\d+)`)
237250

238-
return pushables, pullables, false
251+
return ahead, behind, false
239252
}
240253

241254
func parseDifference(track string, regexStr string) string {

pkg/commands/git_commands/branch_loader_test.go

Lines changed: 57 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -25,89 +25,101 @@ func TestObtainBranch(t *testing.T) {
2525
scenarios := []scenario{
2626
{
2727
testName: "TrimHeads",
28-
input: []string{"", "heads/a_branch", "", "", "subject", "123", timeStamp},
28+
input: []string{"", "heads/a_branch", "", "", "", "subject", "123", timeStamp},
2929
storeCommitDateAsRecency: false,
3030
expectedBranch: &models.Branch{
31-
Name: "a_branch",
32-
Pushables: "?",
33-
Pullables: "?",
34-
Head: false,
35-
Subject: "subject",
36-
CommitHash: "123",
31+
Name: "a_branch",
32+
AheadForPull: "?",
33+
BehindForPull: "?",
34+
AheadForPush: "?",
35+
BehindForPush: "?",
36+
Head: false,
37+
Subject: "subject",
38+
CommitHash: "123",
3739
},
3840
},
3941
{
4042
testName: "NoUpstream",
41-
input: []string{"", "a_branch", "", "", "subject", "123", timeStamp},
43+
input: []string{"", "a_branch", "", "", "", "subject", "123", timeStamp},
4244
storeCommitDateAsRecency: false,
4345
expectedBranch: &models.Branch{
44-
Name: "a_branch",
45-
Pushables: "?",
46-
Pullables: "?",
47-
Head: false,
48-
Subject: "subject",
49-
CommitHash: "123",
46+
Name: "a_branch",
47+
AheadForPull: "?",
48+
BehindForPull: "?",
49+
AheadForPush: "?",
50+
BehindForPush: "?",
51+
Head: false,
52+
Subject: "subject",
53+
CommitHash: "123",
5054
},
5155
},
5256
{
5357
testName: "IsHead",
54-
input: []string{"*", "a_branch", "", "", "subject", "123", timeStamp},
58+
input: []string{"*", "a_branch", "", "", "", "subject", "123", timeStamp},
5559
storeCommitDateAsRecency: false,
5660
expectedBranch: &models.Branch{
57-
Name: "a_branch",
58-
Pushables: "?",
59-
Pullables: "?",
60-
Head: true,
61-
Subject: "subject",
62-
CommitHash: "123",
61+
Name: "a_branch",
62+
AheadForPull: "?",
63+
BehindForPull: "?",
64+
AheadForPush: "?",
65+
BehindForPush: "?",
66+
Head: true,
67+
Subject: "subject",
68+
CommitHash: "123",
6369
},
6470
},
6571
{
6672
testName: "IsBehindAndAhead",
67-
input: []string{"", "a_branch", "a_remote/a_branch", "[behind 2, ahead 3]", "subject", "123", timeStamp},
73+
input: []string{"", "a_branch", "a_remote/a_branch", "[behind 2, ahead 3]", "[behind 2, ahead 3]", "subject", "123", timeStamp},
6874
storeCommitDateAsRecency: false,
6975
expectedBranch: &models.Branch{
70-
Name: "a_branch",
71-
Pushables: "3",
72-
Pullables: "2",
73-
Head: false,
74-
Subject: "subject",
75-
CommitHash: "123",
76+
Name: "a_branch",
77+
AheadForPull: "3",
78+
BehindForPull: "2",
79+
AheadForPush: "3",
80+
BehindForPush: "2",
81+
Head: false,
82+
Subject: "subject",
83+
CommitHash: "123",
7684
},
7785
},
7886
{
7987
testName: "RemoteBranchIsGone",
80-
input: []string{"", "a_branch", "a_remote/a_branch", "[gone]", "subject", "123", timeStamp},
88+
input: []string{"", "a_branch", "a_remote/a_branch", "[gone]", "[gone]", "subject", "123", timeStamp},
8189
storeCommitDateAsRecency: false,
8290
expectedBranch: &models.Branch{
83-
Name: "a_branch",
84-
UpstreamGone: true,
85-
Pushables: "?",
86-
Pullables: "?",
87-
Head: false,
88-
Subject: "subject",
89-
CommitHash: "123",
91+
Name: "a_branch",
92+
UpstreamGone: true,
93+
AheadForPull: "?",
94+
BehindForPull: "?",
95+
AheadForPush: "?",
96+
BehindForPush: "?",
97+
Head: false,
98+
Subject: "subject",
99+
CommitHash: "123",
90100
},
91101
},
92102
{
93103
testName: "WithCommitDateAsRecency",
94-
input: []string{"", "a_branch", "", "", "subject", "123", timeStamp},
104+
input: []string{"", "a_branch", "", "", "", "subject", "123", timeStamp},
95105
storeCommitDateAsRecency: true,
96106
expectedBranch: &models.Branch{
97-
Name: "a_branch",
98-
Recency: "2h",
99-
Pushables: "?",
100-
Pullables: "?",
101-
Head: false,
102-
Subject: "subject",
103-
CommitHash: "123",
107+
Name: "a_branch",
108+
Recency: "2h",
109+
AheadForPull: "?",
110+
BehindForPull: "?",
111+
AheadForPush: "?",
112+
BehindForPush: "?",
113+
Head: false,
114+
Subject: "subject",
115+
CommitHash: "123",
104116
},
105117
},
106118
}
107119

108120
for _, s := range scenarios {
109121
t.Run(s.testName, func(t *testing.T) {
110-
branch := obtainBranch(s.input, s.storeCommitDateAsRecency)
122+
branch := obtainBranch(s.input, s.storeCommitDateAsRecency, true)
111123
assert.EqualValues(t, s.expectedBranch, branch)
112124
})
113125
}

pkg/commands/models/branch.go

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,14 @@ type Branch struct {
1010
DisplayName string
1111
// indicator of when the branch was last checked out e.g. '2d', '3m'
1212
Recency string
13-
// how many commits ahead we are from the remote branch (how many commits we can push)
14-
Pushables string
13+
// how many commits ahead we are from the remote branch (how many commits we can push, assuming we push to our tracked remote branch)
14+
AheadForPull string
1515
// how many commits behind we are from the remote branch (how many commits we can pull)
16-
Pullables string
16+
BehindForPull string
17+
// 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)
18+
AheadForPush string
19+
// 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)
20+
BehindForPush string
1721
// whether the remote branch is 'gone' i.e. we're tracking a remote branch that has been deleted
1822
UpstreamGone bool
1923
// whether this is the current branch. Exactly one branch should have this be true
@@ -80,26 +84,30 @@ func (b *Branch) IsTrackingRemote() bool {
8084
// we know that the remote branch is not stored locally based on our pushable/pullable
8185
// count being question marks.
8286
func (b *Branch) RemoteBranchStoredLocally() bool {
83-
return b.IsTrackingRemote() && b.Pushables != "?" && b.Pullables != "?"
87+
return b.IsTrackingRemote() && b.AheadForPull != "?" && b.BehindForPull != "?"
8488
}
8589

8690
func (b *Branch) RemoteBranchNotStoredLocally() bool {
87-
return b.IsTrackingRemote() && b.Pushables == "?" && b.Pullables == "?"
91+
return b.IsTrackingRemote() && b.AheadForPull == "?" && b.BehindForPull == "?"
8892
}
8993

9094
func (b *Branch) MatchesUpstream() bool {
91-
return b.RemoteBranchStoredLocally() && b.Pushables == "0" && b.Pullables == "0"
95+
return b.RemoteBranchStoredLocally() && b.AheadForPull == "0" && b.BehindForPull == "0"
9296
}
9397

94-
func (b *Branch) HasCommitsToPush() bool {
95-
return b.RemoteBranchStoredLocally() && b.Pushables != "0"
98+
func (b *Branch) IsAheadForPull() bool {
99+
return b.RemoteBranchStoredLocally() && b.AheadForPull != "0"
96100
}
97101

98-
func (b *Branch) HasCommitsToPull() bool {
99-
return b.RemoteBranchStoredLocally() && b.Pullables != "0"
102+
func (b *Branch) IsBehindForPull() bool {
103+
return b.RemoteBranchStoredLocally() && b.BehindForPull != "0"
104+
}
105+
106+
func (b *Branch) IsBehindForPush() bool {
107+
return b.BehindForPush != "" && b.BehindForPush != "0"
100108
}
101109

102110
// for when we're in a detached head state
103111
func (b *Branch) IsRealBranch() bool {
104-
return b.Pushables != "" && b.Pullables != ""
112+
return b.AheadForPull != "" && b.BehindForPull != ""
105113
}

pkg/gui/controllers/branches_controller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -620,7 +620,7 @@ func (self *BranchesController) fastForward(branch *models.Branch) error {
620620
if !branch.RemoteBranchStoredLocally() {
621621
return errors.New(self.c.Tr.FwdNoLocalUpstream)
622622
}
623-
if branch.HasCommitsToPush() {
623+
if branch.IsAheadForPull() {
624624
return errors.New(self.c.Tr.FwdCommitsToPush)
625625
}
626626

pkg/gui/controllers/sync_controller.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,10 @@ func (self *SyncController) branchCheckedOut(f func(*models.Branch) error) func(
8787
}
8888

8989
func (self *SyncController) push(currentBranch *models.Branch) error {
90-
// if we have pullables we'll ask if the user wants to force push
90+
// if we are behind our upstream branch we'll ask if the user wants to force push
9191
if currentBranch.IsTrackingRemote() {
9292
opts := pushOpts{}
93-
if currentBranch.HasCommitsToPull() {
93+
if currentBranch.IsBehindForPush() {
9494
return self.requestToForcePush(currentBranch, opts)
9595
} else {
9696
return self.pushAux(currentBranch, opts)

pkg/gui/presentation/branches.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,11 +196,11 @@ func BranchStatus(
196196
}
197197

198198
result := ""
199-
if branch.HasCommitsToPush() {
200-
result = fmt.Sprintf("↑%s", branch.Pushables)
199+
if branch.IsAheadForPull() {
200+
result = fmt.Sprintf("↑%s", branch.AheadForPull)
201201
}
202-
if branch.HasCommitsToPull() {
203-
result = fmt.Sprintf("%s↓%s", result, branch.Pullables)
202+
if branch.IsBehindForPull() {
203+
result = fmt.Sprintf("%s↓%s", result, branch.BehindForPull)
204204
}
205205

206206
return result

0 commit comments

Comments
 (0)