1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-05-17 22:32:58 +02:00

Fix pushing to branch when upstream not stored locally (#3619)

- **PR Description**

This fixes three different problems; two were recent regressions, one
has never worked.

1. For branches whose remote is not stored locally, don't ask to force
push right away. Try a normal push first. This broke with #3528.
2. For branches whose remote is not stored locally (but only for those),
if the server rejects the update, ask to force push. This broke with
#3387.
3. When force-pushing a branch whose remote is not stored locally, use
`--force` instead of `--force-with-lease`; otherwise the push would
always fail with a "stale info" error. This has never worked.

Fixes #3588.
This commit is contained in:
Stefan Haller 2024-06-01 08:15:35 +02:00 committed by GitHub
commit c5baa5da3a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 1485 additions and 1360 deletions

View File

@ -19,6 +19,7 @@ func NewSyncCommands(gitCommon *GitCommon) *SyncCommands {
// Push pushes to a branch
type PushOpts struct {
Force bool
ForceWithLease bool
UpstreamRemote string
UpstreamBranch string
SetUpstream bool
@ -30,7 +31,8 @@ func (self *SyncCommands) PushCmdObj(task gocui.Task, opts PushOpts) (oscommands
}
cmdArgs := NewGitCmd("push").
ArgIf(opts.Force, "--force-with-lease").
ArgIf(opts.Force, "--force").
ArgIf(opts.ForceWithLease, "--force-with-lease").
ArgIf(opts.SetUpstream, "--set-upstream").
ArgIf(opts.UpstreamRemote != "", opts.UpstreamRemote).
ArgIf(opts.UpstreamBranch != "", opts.UpstreamBranch).

View File

@ -18,24 +18,32 @@ func TestSyncPush(t *testing.T) {
scenarios := []scenario{
{
testName: "Push with force disabled",
opts: PushOpts{Force: false},
opts: PushOpts{ForceWithLease: false},
test: func(cmdObj oscommands.ICmdObj, err error) {
assert.Equal(t, cmdObj.Args(), []string{"git", "push"})
assert.NoError(t, err)
},
},
{
testName: "Push with force-with-lease enabled",
opts: PushOpts{ForceWithLease: true},
test: func(cmdObj oscommands.ICmdObj, err error) {
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "--force-with-lease"})
assert.NoError(t, err)
},
},
{
testName: "Push with force enabled",
opts: PushOpts{Force: true},
test: func(cmdObj oscommands.ICmdObj, err error) {
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "--force-with-lease"})
assert.Equal(t, cmdObj.Args(), []string{"git", "push", "--force"})
assert.NoError(t, err)
},
},
{
testName: "Push with force disabled, upstream supplied",
opts: PushOpts{
Force: false,
ForceWithLease: false,
UpstreamRemote: "origin",
UpstreamBranch: "master",
},
@ -47,7 +55,7 @@ func TestSyncPush(t *testing.T) {
{
testName: "Push with force disabled, setting upstream",
opts: PushOpts{
Force: false,
ForceWithLease: false,
UpstreamRemote: "origin",
UpstreamBranch: "master",
SetUpstream: true,
@ -58,9 +66,9 @@ func TestSyncPush(t *testing.T) {
},
},
{
testName: "Push with force enabled, setting upstream",
testName: "Push with force-with-lease enabled, setting upstream",
opts: PushOpts{
Force: true,
ForceWithLease: true,
UpstreamRemote: "origin",
UpstreamBranch: "master",
SetUpstream: true,
@ -73,7 +81,7 @@ func TestSyncPush(t *testing.T) {
{
testName: "Push with remote branch but no origin",
opts: PushOpts{
Force: true,
ForceWithLease: true,
UpstreamRemote: "",
UpstreamBranch: "master",
SetUpstream: true,

View File

@ -104,7 +104,7 @@ func (b *Branch) IsBehindForPull() bool {
}
func (b *Branch) IsBehindForPush() bool {
return b.BehindForPush != "" && b.BehindForPush != "0"
return b.RemoteBranchStoredLocally() && b.BehindForPush != "0"
}
// for when we're in a detached head state

View File

@ -89,7 +89,7 @@ func (self *SyncController) branchCheckedOut(f func(*models.Branch) error) func(
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{}
opts := pushOpts{remoteBranchStoredLocally: currentBranch.RemoteBranchStoredLocally()}
if currentBranch.IsBehindForPush() {
return self.requestToForcePush(currentBranch, opts)
} else {
@ -180,9 +180,16 @@ func (self *SyncController) pullWithLock(task gocui.Task, opts PullFilesOptions)
type pushOpts struct {
force bool
forceWithLease bool
upstreamRemote string
upstreamBranch string
setUpstream bool
// If this is false, we can't tell ahead of time whether a force-push will
// be necessary, so we start with a normal push and offer to force-push if
// the server rejected. If this is true, we don't offer to force-push if the
// server rejected, but rather ask the user to fetch.
remoteBranchStoredLocally bool
}
func (self *SyncController) pushAux(currentBranch *models.Branch, opts pushOpts) error {
@ -192,14 +199,33 @@ func (self *SyncController) pushAux(currentBranch *models.Branch, opts pushOpts)
task,
git_commands.PushOpts{
Force: opts.force,
ForceWithLease: opts.forceWithLease,
UpstreamRemote: opts.upstreamRemote,
UpstreamBranch: opts.upstreamBranch,
SetUpstream: opts.setUpstream,
})
if err != nil {
if strings.Contains(err.Error(), "Updates were rejected") {
if !opts.force && !opts.forceWithLease && strings.Contains(err.Error(), "Updates were rejected") {
if opts.remoteBranchStoredLocally {
return errors.New(self.c.Tr.UpdatesRejected)
}
forcePushDisabled := self.c.UserConfig.Git.DisableForcePushing
if forcePushDisabled {
return errors.New(self.c.Tr.UpdatesRejectedAndForcePushDisabled)
}
_ = self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.ForcePush,
Prompt: self.forcePushPrompt(),
HandleConfirm: func() error {
newOpts := opts
newOpts.force = true
return self.pushAux(currentBranch, newOpts)
},
})
return nil
}
return err
}
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
@ -216,7 +242,7 @@ func (self *SyncController) requestToForcePush(currentBranch *models.Branch, opt
Title: self.c.Tr.ForcePush,
Prompt: self.forcePushPrompt(),
HandleConfirm: func() error {
opts.force = true
opts.forceWithLease = true
return self.pushAux(currentBranch, opts)
},
})

View File

@ -132,6 +132,7 @@ func chineseTranslationSet() TranslationSet {
ForcePush: "强制推送",
ForcePushPrompt: "您的分支已与远程分支不同。按‘esc’取消,或‘enter’强制推送.",
ForcePushDisabled: "您的分支已与远程分支不同, 并且您已经禁用了强行推送",
UpdatesRejectedAndForcePushDisabled: "更新被拒绝,您已禁用强制推送",
CheckForUpdate: "检查更新",
CheckingForUpdates: "正在检查更新…",
OnLatestVersionErr: "已是最新版本",

View File

@ -198,6 +198,7 @@ type TranslationSet struct {
ForcePushPrompt string
ForcePushDisabled string
UpdatesRejected string
UpdatesRejectedAndForcePushDisabled string
CheckForUpdate string
CheckingForUpdates string
UpdateAvailableTitle string
@ -1160,6 +1161,7 @@ func EnglishTranslationSet() TranslationSet {
ForcePushPrompt: "Your branch has diverged from the remote branch. Press {{.cancelKey}} to cancel, or {{.confirmKey}} to force push.",
ForcePushDisabled: "Your branch has diverged from the remote branch and you've disabled force pushing",
UpdatesRejected: "Updates were rejected. Please fetch and examine the remote changes before pushing again.",
UpdatesRejectedAndForcePushDisabled: "Updates were rejected and you have disabled force pushing",
CheckForUpdate: "Check for update",
CheckingForUpdates: "Checking for updates...",
UpdateAvailableTitle: "Update available!",

View File

@ -124,6 +124,7 @@ func koreanTranslationSet() TranslationSet {
ForcePush: "강제 푸시",
ForcePushPrompt: "브랜치가 원격 브랜치에서 분기하고 있습니다. 'esc'를 눌러 취소하거나, 'enter'를 눌러 강제로 푸시하세요.",
ForcePushDisabled: "브랜치가 원격 브랜치에서 분기하고 있습니다. force push가 비활성화 되었습니다.",
UpdatesRejectedAndForcePushDisabled: "업데이트가 거부되었으며 강제 푸시를 비활성화했습니다.",
CheckForUpdate: "업데이트 확인",
CheckingForUpdates: "업데이트 확인 중...",
UpdateAvailableTitle: "새로운 업데이트 사용가능!",

View File

@ -183,6 +183,7 @@ func polishTranslationSet() TranslationSet {
ForcePush: "Wymuś wysłanie",
ForcePushPrompt: "Twoja gałąź rozbiegła się z gałęzią zdalną. Naciśnij {{.cancelKey}}, aby anulować, lub {{.confirmKey}}, aby wymusić wysłanie.",
ForcePushDisabled: "Twoja gałąź rozbiegła się z gałęzią zdalną i masz wyłączone wymuszanie wysyłania",
UpdatesRejectedAndForcePushDisabled: "Aktualizacje zostały odrzucone i wyłączyłeś wymuszenie wysłania",
CheckForUpdate: "Sprawdź aktualizacje",
CheckingForUpdates: "Sprawdzanie aktualizacji...",
UpdateAvailableTitle: "Dostępna aktualizacja!",

View File

@ -152,6 +152,7 @@ func RussianTranslationSet() TranslationSet {
ForcePush: "Принудительная отправка изменении",
ForcePushPrompt: "Ветка отклонилась от удалённой ветки. Нажмите «esc», чтобы отменить, или «enter», чтобы начать принудительную отправку изменении.",
ForcePushDisabled: "Ветка отклонилась от удалённой ветки. Принудительная отправка изменении была отключена",
UpdatesRejectedAndForcePushDisabled: "Обновления были отклонены. Принудительная отправка изменении была отключена",
CheckForUpdate: "Проверить обновления",
CheckingForUpdates: "Проверка обновлений...",
UpdateAvailableTitle: "Доступно обновление!",

View File

@ -184,6 +184,7 @@ func traditionalChineseTranslationSet() TranslationSet {
ForcePush: "強制推送",
ForcePushPrompt: "你的分支與遠端分支分岔。按 'ESC' 取消,或按 'Enter' 強制推送。",
ForcePushDisabled: "你的分支與遠端分支分岔,你已禁用強制推送",
UpdatesRejectedAndForcePushDisabled: "更新被拒絕,你已禁用強制推送",
CheckForUpdate: "檢查更新",
CheckingForUpdates: "正在檢查更新...",
UpdateAvailableTitle: "有可用的更新!",

View File

@ -0,0 +1,81 @@
package sync
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var ForcePushRemoteBranchNotStoredLocally = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Push a branch whose remote branch is not stored locally, requiring a force push",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.EmptyCommit("one")
shell.EmptyCommit("two")
shell.Clone("some-remote")
// remove the 'two' commit so that we have something to pull from the remote
shell.HardReset("HEAD^")
shell.SetConfig("branch.master.remote", "../some-remote")
shell.SetConfig("branch.master.pushRemote", "../some-remote")
shell.SetConfig("branch.master.merge", "refs/heads/master")
shell.CreateFileAndAdd("file1", "file1 content")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Lines(
Contains("one"),
)
t.Views().Status().Content(Contains("? repo → master"))
// We're behind our upstream now, so we expect to be asked to force-push
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()
// Make a new local commit
t.Views().Files().IsFocused().Press(keys.Files.CommitChanges)
t.ExpectPopup().CommitMessagePanel().Type("new").Confirm()
t.Views().Commits().
Lines(
Contains("new"),
Contains("one"),
)
// Pushing this works without needing to force push
t.Views().Files().IsFocused().Press(keys.Universal.Push)
// Now add the clone as a remote just so that we can check if what we
// pushed arrived there correctly
t.Views().Remotes().Focus().
Press(keys.Universal.New)
t.ExpectPopup().Prompt().
Title(Equals("New remote name:")).Type("some-remote").Confirm()
t.ExpectPopup().Prompt().
Title(Equals("New remote url:")).Type("../some-remote").Confirm()
t.Views().Remotes().Lines(
Contains("some-remote").IsSelected(),
).
PressEnter()
t.Views().RemoteBranches().IsFocused().Lines(
Contains("master").IsSelected(),
).
PressEnter()
t.Views().SubCommits().IsFocused().Lines(
Contains("new"),
Contains("one"),
)
},
})

View File

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