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:
commit
c5baa5da3a
@ -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).
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
},
|
||||
})
|
||||
|
@ -132,6 +132,7 @@ func chineseTranslationSet() TranslationSet {
|
||||
ForcePush: "强制推送",
|
||||
ForcePushPrompt: "您的分支已与远程分支不同。按‘esc’取消,或‘enter’强制推送.",
|
||||
ForcePushDisabled: "您的分支已与远程分支不同, 并且您已经禁用了强行推送",
|
||||
UpdatesRejectedAndForcePushDisabled: "更新被拒绝,您已禁用强制推送",
|
||||
CheckForUpdate: "检查更新",
|
||||
CheckingForUpdates: "正在检查更新…",
|
||||
OnLatestVersionErr: "已是最新版本",
|
||||
|
@ -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!",
|
||||
|
@ -124,6 +124,7 @@ func koreanTranslationSet() TranslationSet {
|
||||
ForcePush: "강제 푸시",
|
||||
ForcePushPrompt: "브랜치가 원격 브랜치에서 분기하고 있습니다. 'esc'를 눌러 취소하거나, 'enter'를 눌러 강제로 푸시하세요.",
|
||||
ForcePushDisabled: "브랜치가 원격 브랜치에서 분기하고 있습니다. force push가 비활성화 되었습니다.",
|
||||
UpdatesRejectedAndForcePushDisabled: "업데이트가 거부되었으며 강제 푸시를 비활성화했습니다.",
|
||||
CheckForUpdate: "업데이트 확인",
|
||||
CheckingForUpdates: "업데이트 확인 중...",
|
||||
UpdateAvailableTitle: "새로운 업데이트 사용가능!",
|
||||
|
@ -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!",
|
||||
|
@ -152,6 +152,7 @@ func RussianTranslationSet() TranslationSet {
|
||||
ForcePush: "Принудительная отправка изменении",
|
||||
ForcePushPrompt: "Ветка отклонилась от удалённой ветки. Нажмите «esc», чтобы отменить, или «enter», чтобы начать принудительную отправку изменении.",
|
||||
ForcePushDisabled: "Ветка отклонилась от удалённой ветки. Принудительная отправка изменении была отключена",
|
||||
UpdatesRejectedAndForcePushDisabled: "Обновления были отклонены. Принудительная отправка изменении была отключена",
|
||||
CheckForUpdate: "Проверить обновления",
|
||||
CheckingForUpdates: "Проверка обновлений...",
|
||||
UpdateAvailableTitle: "Доступно обновление!",
|
||||
|
@ -184,6 +184,7 @@ func traditionalChineseTranslationSet() TranslationSet {
|
||||
ForcePush: "強制推送",
|
||||
ForcePushPrompt: "你的分支與遠端分支分岔。按 'ESC' 取消,或按 'Enter' 強制推送。",
|
||||
ForcePushDisabled: "你的分支與遠端分支分岔,你已禁用強制推送",
|
||||
UpdatesRejectedAndForcePushDisabled: "更新被拒絕,你已禁用強制推送",
|
||||
CheckForUpdate: "檢查更新",
|
||||
CheckingForUpdates: "正在檢查更新...",
|
||||
UpdateAvailableTitle: "有可用的更新!",
|
||||
|
@ -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"),
|
||||
)
|
||||
},
|
||||
})
|
@ -276,6 +276,7 @@ var tests = []*components.IntegrationTest{
|
||||
sync.ForcePush,
|
||||
sync.ForcePushMultipleMatching,
|
||||
sync.ForcePushMultipleUpstream,
|
||||
sync.ForcePushRemoteBranchNotStoredLocally,
|
||||
sync.ForcePushTriangular,
|
||||
sync.Pull,
|
||||
sync.PullAndSetUpstream,
|
||||
|
Loading…
x
Reference in New Issue
Block a user