From 6378618544fb10d4890e2db4ebc4f9236e6287d4 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 20 Sep 2026 16:40:32 +0200 Subject: [PATCH] Generalize the push command to take a remote and a list of refspecs PushOpts can only describe a push of the current branch. It takes the branch's name and the upstream to push it to, and builds a single refspec from them. Pushing several branches in one command needs one refspec per branch, so let callers pass the refspecs themselves. The sync controller, the only caller so far, builds the same single refspec that PushCmdObj built before. Co-Authored-By: Claude Fable 5.1 --- pkg/commands/git_commands/sync.go | 19 +++++++------- pkg/commands/git_commands/sync_test.go | 35 ++++++++++++++++---------- pkg/gui/controllers/sync_controller.go | 9 ++++--- 3 files changed, 38 insertions(+), 25 deletions(-) diff --git a/pkg/commands/git_commands/sync.go b/pkg/commands/git_commands/sync.go index 0400b4e26..1a20a3293 100644 --- a/pkg/commands/git_commands/sync.go +++ b/pkg/commands/git_commands/sync.go @@ -1,8 +1,6 @@ package git_commands import ( - "fmt" - "github.com/go-errors/errors" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/gocui" @@ -18,18 +16,21 @@ func NewSyncCommands(gitCommon *GitCommon) *SyncCommands { } } -// Push pushes to a branch type PushOpts struct { Force bool ForceWithLease bool - CurrentBranch string - UpstreamRemote string - UpstreamBranch string SetUpstream bool + // The remote to push to. If empty, git picks it from its configuration, + // and Refspecs must be empty too. + Remote string + // What to push, each in the form "refs/heads/:". + // If empty, git decides what to push based on push.default and + // remote..push. + Refspecs []string } func (self *SyncCommands) PushCmdObj(task gocui.Task, opts PushOpts) (*oscommands.CmdObj, error) { - if opts.UpstreamBranch != "" && opts.UpstreamRemote == "" { + if len(opts.Refspecs) > 0 && opts.Remote == "" { return nil, errors.New(self.Tr.MustSpecifyOriginError) } @@ -37,8 +38,8 @@ func (self *SyncCommands) PushCmdObj(task gocui.Task, opts PushOpts) (*oscommand ArgIf(opts.Force, "--force"). ArgIf(opts.ForceWithLease, "--force-with-lease"). ArgIf(opts.SetUpstream, "--set-upstream"). - ArgIf(opts.UpstreamRemote != "", opts.UpstreamRemote). - ArgIf(opts.UpstreamBranch != "", fmt.Sprintf("refs/heads/%s:%s", opts.CurrentBranch, opts.UpstreamBranch)). + ArgIf(opts.Remote != "", opts.Remote). + Arg(opts.Refspecs...). ToArgv() cmdObj := self.cmd.New(cmdArgs).PromptOnCredentialRequest(task) diff --git a/pkg/commands/git_commands/sync_test.go b/pkg/commands/git_commands/sync_test.go index 6a7702586..82dcf2801 100644 --- a/pkg/commands/git_commands/sync_test.go +++ b/pkg/commands/git_commands/sync_test.go @@ -41,12 +41,11 @@ func TestSyncPush(t *testing.T) { }, }, { - testName: "Push with force disabled, upstream supplied", + testName: "Push with force disabled, refspec supplied", opts: PushOpts{ ForceWithLease: false, - CurrentBranch: "master", - UpstreamRemote: "origin", - UpstreamBranch: "master", + Remote: "origin", + Refspecs: []string{"refs/heads/master:master"}, }, test: func(cmdObj *oscommands.CmdObj, err error) { assert.Equal(t, cmdObj.Args(), []string{"git", "push", "origin", "refs/heads/master:master"}) @@ -57,9 +56,8 @@ func TestSyncPush(t *testing.T) { testName: "Push with force disabled, setting upstream", opts: PushOpts{ ForceWithLease: false, - CurrentBranch: "master-local", - UpstreamRemote: "origin", - UpstreamBranch: "master", + Remote: "origin", + Refspecs: []string{"refs/heads/master-local:master"}, SetUpstream: true, }, test: func(cmdObj *oscommands.CmdObj, err error) { @@ -71,9 +69,8 @@ func TestSyncPush(t *testing.T) { testName: "Push with force-with-lease enabled, setting upstream", opts: PushOpts{ ForceWithLease: true, - CurrentBranch: "master", - UpstreamRemote: "origin", - UpstreamBranch: "master", + Remote: "origin", + Refspecs: []string{"refs/heads/master:master"}, SetUpstream: true, }, test: func(cmdObj *oscommands.CmdObj, err error) { @@ -82,11 +79,23 @@ func TestSyncPush(t *testing.T) { }, }, { - testName: "Push with remote branch but no origin", + testName: "Push several refspecs", opts: PushOpts{ ForceWithLease: true, - UpstreamRemote: "", - UpstreamBranch: "master", + Remote: "origin", + Refspecs: []string{"refs/heads/a:refs/heads/a", "refs/heads/b:refs/heads/b"}, + }, + test: func(cmdObj *oscommands.CmdObj, err error) { + assert.Equal(t, cmdObj.Args(), []string{"git", "push", "--force-with-lease", "origin", "refs/heads/a:refs/heads/a", "refs/heads/b:refs/heads/b"}) + assert.NoError(t, err) + }, + }, + { + testName: "Push with refspec but no remote", + opts: PushOpts{ + ForceWithLease: true, + Remote: "", + Refspecs: []string{"refs/heads/master:master"}, SetUpstream: true, }, test: func(cmdObj *oscommands.CmdObj, err error) { diff --git a/pkg/gui/controllers/sync_controller.go b/pkg/gui/controllers/sync_controller.go index 61b92747b..d787a54a8 100644 --- a/pkg/gui/controllers/sync_controller.go +++ b/pkg/gui/controllers/sync_controller.go @@ -195,15 +195,18 @@ type pushOpts struct { func (self *SyncController) pushAux(currentBranch *models.Branch, opts pushOpts) error { return self.c.WithInlineStatus(currentBranch, types.ItemOperationPushing, context.LOCAL_BRANCHES_CONTEXT_KEY, func(task gocui.Task) error { self.c.LogAction(self.c.Tr.Actions.Push) + refspecs := []string{} + if opts.upstreamBranch != "" { + refspecs = append(refspecs, fmt.Sprintf("refs/heads/%s:%s", currentBranch.Name, opts.upstreamBranch)) + } err := self.c.Git().Sync.Push( task, git_commands.PushOpts{ Force: opts.force, ForceWithLease: opts.forceWithLease, - CurrentBranch: currentBranch.Name, - UpstreamRemote: opts.upstreamRemote, - UpstreamBranch: opts.upstreamBranch, SetUpstream: opts.setUpstream, + Remote: opts.upstreamRemote, + Refspecs: refspecs, }) if err != nil { if !opts.force && !opts.forceWithLease && strings.Contains(err.Error(), "Updates were rejected") {