mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-13 09:06:27 -04:00
* Add a keybinding to push the selected branch in the Branches panel Push (shift+P) always targeted the checked-out branch, even with a different branch highlighted in the Branches panel. That made pushing any other local branch require checking it out first. git push with no refspec always targets whatever is checked out, so reaching a non-current branch requires naming it explicitly in the refspec. Add that explicit-refspec path and wire a new keybinding in the Branches panel that pushes whichever branch is selected, leaving the existing global push (used from any other panel) untouched. * Add a keybinding to pull the selected branch's linked worktree Real git has no way to merge or rebase into a branch that isn't checked out anywhere - that always needs a working tree. Fast-forward (already available via the F key) covers the clean case, but a diverged branch needs a real pull, which is only possible if the branch has its own linked worktree. Add p in the Branches panel for that case: it runs the pull in the branch's linked worktree rather than the currently open one, and is disabled when the branch has no worktree. If the pull hits a conflict in a worktree other than the one currently open, we can't drop the user into the interactive conflict-resolution flow (that UI operates on the open worktree), so we report the failure and point them at switching to it instead. * Remove the No PRs rule from AGENTS.md That rule guarded against an agent opening PRs against the upstream jesseduffield/lazygit repo. This is a personal fork with its own workflow, so the guard no longer applies here. --------- Co-authored-by: Khanh Truong <khanh_t@arbin.com>
343 lines
10 KiB
Go
343 lines
10 KiB
Go
package controllers
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
|
"github.com/jesseduffield/lazygit/pkg/gocui"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
)
|
|
|
|
type SyncController struct {
|
|
baseController
|
|
c *ControllerCommon
|
|
}
|
|
|
|
var _ types.IController = &SyncController{}
|
|
|
|
func NewSyncController(
|
|
common *ControllerCommon,
|
|
) *SyncController {
|
|
return &SyncController{
|
|
baseController: baseController{},
|
|
c: common,
|
|
}
|
|
}
|
|
|
|
func (self *SyncController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
|
|
bindings := []*types.Binding{
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.Push),
|
|
Handler: opts.Guards.NoPopupPanel(self.HandlePush),
|
|
GetDisabledReason: self.getDisabledReasonForPushOrPull,
|
|
Description: self.c.Tr.Push,
|
|
Tooltip: self.c.Tr.PushTooltip,
|
|
},
|
|
{
|
|
Keys: opts.GetKeys(opts.Config.Universal.Pull),
|
|
Handler: opts.Guards.NoPopupPanel(self.HandlePull),
|
|
GetDisabledReason: self.getDisabledReasonForPushOrPull,
|
|
Description: self.c.Tr.Pull,
|
|
Tooltip: self.c.Tr.PullTooltip,
|
|
},
|
|
}
|
|
|
|
return bindings
|
|
}
|
|
|
|
func (self *SyncController) Context() types.Context {
|
|
return nil
|
|
}
|
|
|
|
func (self *SyncController) HandlePush() error {
|
|
return self.branchCheckedOut(func(branch *models.Branch) error {
|
|
return self.push(branch, false)
|
|
})()
|
|
}
|
|
|
|
// PushBranch pushes the given branch, regardless of whether it's currently
|
|
// checked out. Unlike HandlePush, this always targets the branch explicitly
|
|
// (via its own upstream), rather than relying on `git push` implicitly
|
|
// pushing whatever is checked out.
|
|
func (self *SyncController) PushBranch(branch *models.Branch) error {
|
|
return self.push(branch, true)
|
|
}
|
|
|
|
func (self *SyncController) HandlePull() error {
|
|
return self.branchCheckedOut(self.pull)()
|
|
}
|
|
|
|
func (self *SyncController) getDisabledReasonForPushOrPull() *types.DisabledReason {
|
|
currentBranch := self.c.Helpers().Refs.GetCheckedOutRef()
|
|
if currentBranch != nil {
|
|
op := self.c.State().GetItemOperation(currentBranch)
|
|
if op != types.ItemOperationNone {
|
|
return &types.DisabledReason{Text: self.c.Tr.CantPullOrPushSameBranchTwice}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (self *SyncController) branchCheckedOut(f func(*models.Branch) error) func() error {
|
|
return func() error {
|
|
currentBranch := self.c.Helpers().Refs.GetCheckedOutRef()
|
|
if currentBranch == nil {
|
|
// need to wait for branches to refresh
|
|
return nil
|
|
}
|
|
|
|
return f(currentBranch)
|
|
}
|
|
}
|
|
|
|
// explicitTarget forces the push to name the branch and its upstream
|
|
// explicitly in the refspec, rather than relying on `git push` implicitly
|
|
// targeting whatever is currently checked out. This is required whenever
|
|
// currentBranch might not be the checked-out branch (e.g. pushing a branch
|
|
// selected in the Branches panel).
|
|
func (self *SyncController) push(currentBranch *models.Branch, explicitTarget bool) error {
|
|
// if we are behind our upstream branch we'll ask if the user wants to force push
|
|
if currentBranch.IsTrackingRemote() {
|
|
opts := pushOpts{remoteBranchStoredLocally: currentBranch.RemoteBranchStoredLocally()}
|
|
if explicitTarget {
|
|
opts.upstreamRemote = currentBranch.UpstreamRemote
|
|
opts.upstreamBranch = currentBranch.UpstreamBranch
|
|
}
|
|
if currentBranch.IsBehindForPush() {
|
|
return self.requestToForcePush(currentBranch, opts)
|
|
}
|
|
|
|
return self.pushAux(currentBranch, opts)
|
|
}
|
|
|
|
if !explicitTarget && self.c.Git().Config.GetPushToCurrent() {
|
|
return self.pushAux(currentBranch, pushOpts{setUpstream: true})
|
|
}
|
|
|
|
return self.c.Helpers().Upstream.PromptForUpstreamWithInitialContent(currentBranch, func(upstream string) error {
|
|
upstreamRemote, upstreamBranch, err := self.c.Helpers().Upstream.ParseUpstream(upstream)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return self.pushAux(currentBranch, pushOpts{
|
|
setUpstream: true,
|
|
upstreamRemote: upstreamRemote,
|
|
upstreamBranch: upstreamBranch,
|
|
})
|
|
})
|
|
}
|
|
|
|
func (self *SyncController) pull(currentBranch *models.Branch) error {
|
|
action := self.c.Tr.Actions.Pull
|
|
|
|
// if we have no upstream branch we need to set that first
|
|
if !currentBranch.IsTrackingRemote() {
|
|
return self.c.Helpers().Upstream.PromptForUpstreamWithInitialContent(currentBranch, func(upstream string) error {
|
|
if err := self.setCurrentBranchUpstream(upstream); err != nil {
|
|
return err
|
|
}
|
|
|
|
return self.PullAux(currentBranch, PullFilesOptions{Action: action})
|
|
})
|
|
}
|
|
|
|
return self.PullAux(currentBranch, PullFilesOptions{Action: action})
|
|
}
|
|
|
|
// PullBranch pulls the given branch via its linked worktree (which may or
|
|
// may not be the currently open one). The caller is responsible for
|
|
// confirming the branch has a linked worktree at all.
|
|
func (self *SyncController) PullBranch(branch *models.Branch, worktree *models.Worktree) error {
|
|
action := self.c.Tr.Actions.Pull
|
|
|
|
worktreeGitDir := ""
|
|
worktreePath := ""
|
|
if !worktree.IsCurrent {
|
|
worktreeGitDir = worktree.GitDir
|
|
worktreePath = worktree.Path
|
|
}
|
|
|
|
opts := PullFilesOptions{
|
|
Action: action,
|
|
WorktreeGitDir: worktreeGitDir,
|
|
WorktreePath: worktreePath,
|
|
IsCurrentWorktree: worktree.IsCurrent,
|
|
}
|
|
|
|
if !branch.IsTrackingRemote() {
|
|
return self.c.Helpers().Upstream.PromptForUpstreamWithInitialContent(branch, func(upstream string) error {
|
|
upstreamRemote, upstreamBranch, err := self.c.Helpers().Upstream.ParseUpstream(upstream)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := self.c.Git().Branch.SetUpstream(upstreamRemote, upstreamBranch, branch.Name); err != nil {
|
|
return err
|
|
}
|
|
|
|
return self.PullAux(branch, opts)
|
|
})
|
|
}
|
|
|
|
return self.PullAux(branch, opts)
|
|
}
|
|
|
|
func (self *SyncController) setCurrentBranchUpstream(upstream string) error {
|
|
upstreamRemote, upstreamBranch, err := self.c.Helpers().Upstream.ParseUpstream(upstream)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := self.c.Git().Branch.SetCurrentBranchUpstream(upstreamRemote, upstreamBranch); err != nil {
|
|
if strings.Contains(err.Error(), "does not exist") {
|
|
return fmt.Errorf(
|
|
"upstream branch %s/%s not found.\nIf you expect it to exist, you should fetch (with 'f').\nOtherwise, you should push (with 'shift+P')",
|
|
upstreamRemote, upstreamBranch,
|
|
)
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type PullFilesOptions struct {
|
|
UpstreamRemote string
|
|
UpstreamBranch string
|
|
FastForwardOnly bool
|
|
Action string
|
|
|
|
// Set when pulling a branch that isn't checked out in the current
|
|
// worktree, but has its own linked worktree elsewhere. Empty for the
|
|
// current worktree.
|
|
WorktreeGitDir string
|
|
WorktreePath string
|
|
// False whenever WorktreeGitDir/WorktreePath are set.
|
|
IsCurrentWorktree bool
|
|
}
|
|
|
|
func (self *SyncController) PullAux(currentBranch *models.Branch, opts PullFilesOptions) error {
|
|
return self.c.WithInlineStatus(currentBranch, types.ItemOperationPulling, context.LOCAL_BRANCHES_CONTEXT_KEY, func(task gocui.Task) error {
|
|
return self.pullWithLock(task, opts)
|
|
})
|
|
}
|
|
|
|
func (self *SyncController) pullWithLock(task gocui.Task, opts PullFilesOptions) error {
|
|
self.c.LogAction(opts.Action)
|
|
|
|
err := self.c.Git().Sync.Pull(
|
|
task,
|
|
git_commands.PullOptions{
|
|
RemoteName: opts.UpstreamRemote,
|
|
BranchName: opts.UpstreamBranch,
|
|
FastForwardOnly: opts.FastForwardOnly,
|
|
WorktreeGitDir: opts.WorktreeGitDir,
|
|
WorktreePath: opts.WorktreePath,
|
|
},
|
|
)
|
|
|
|
if !opts.IsCurrentWorktree {
|
|
// We're not looking at this worktree, so we can't drop the user into
|
|
// an interactive rebase/merge-conflict resolution flow for it - that
|
|
// UI operates on the currently open worktree. Report the failure
|
|
// plainly instead and point them at switching to it.
|
|
if err != nil {
|
|
return fmt.Errorf("%s\n\n%s", err.Error(), self.c.Tr.PullFailedInOtherWorktree)
|
|
}
|
|
self.c.RefreshFromWorker(types.RefreshOptions{})
|
|
return nil
|
|
}
|
|
|
|
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebaseAndSelectHeadCommit(err)
|
|
}
|
|
|
|
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 {
|
|
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)
|
|
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,
|
|
})
|
|
if err != nil {
|
|
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
|
|
}
|
|
self.c.RefreshFromWorker(types.RefreshOptions{})
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (self *SyncController) requestToForcePush(currentBranch *models.Branch, opts pushOpts) error {
|
|
forcePushDisabled := self.c.UserConfig().Git.DisableForcePushing
|
|
if forcePushDisabled {
|
|
return errors.New(self.c.Tr.ForcePushDisabled)
|
|
}
|
|
|
|
self.c.Confirm(types.ConfirmOpts{
|
|
Title: self.c.Tr.ForcePush,
|
|
Prompt: self.forcePushPrompt(),
|
|
HandleConfirm: func() error {
|
|
opts.forceWithLease = true
|
|
return self.pushAux(currentBranch, opts)
|
|
},
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (self *SyncController) forcePushPrompt() string {
|
|
return utils.ResolvePlaceholderString(
|
|
self.c.Tr.ForcePushPrompt,
|
|
map[string]string{
|
|
"cancelKey": self.c.UserConfig().Keybinding.Universal.Return.String(),
|
|
"confirmKey": self.c.UserConfig().Keybinding.Universal.Confirm.String(),
|
|
},
|
|
)
|
|
}
|