Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/rebase_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2273,6 +2273,7 @@ func setupAmendedParentRepo(t *testing.T, corruptBase bool) amendedParentRepo {
cloneDir := filepath.Join(t.TempDir(), "clone")

issue250Git(t, ".", "-c", "safe.bareRepository=all", "init", "--bare", "-b", "main", remoteDir)
issue250Git(t, ".", "-c", "safe.bareRepository=all", "-C", remoteDir, "config", "receive.advertisePushOptions", "true")
issue250Git(t, ".", "clone", remoteDir, cloneDir)
issue250Git(t, cloneDir, "config", "user.name", "Test")
issue250Git(t, cloneDir, "config", "user.email", "test@example.com")
Expand Down Expand Up @@ -2410,6 +2411,7 @@ func TestIntegration_AdoptedBranchRebasesFromCommonAncestor(t *testing.T) {
cloneDir := filepath.Join(t.TempDir(), "clone")

issue250Git(t, ".", "-c", "safe.bareRepository=all", "init", "--bare", "-b", "main", remoteDir)
issue250Git(t, ".", "-c", "safe.bareRepository=all", "-C", remoteDir, "config", "receive.advertisePushOptions", "true")
issue250Git(t, ".", "clone", remoteDir, cloneDir)
issue250Git(t, cloneDir, "config", "user.name", "Test")
issue250Git(t, cloneDir, "config", "user.email", "test@example.com")
Expand Down
12 changes: 11 additions & 1 deletion internal/git/gitops.go

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✨

Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,17 @@ func (d *defaultOps) Push(remote string, branches []string, force, atomic bool)
for _, b := range branches {
args = append(args, fmt.Sprintf("refs/heads/%s:refs/heads/%s", b, b))
}
return d.runSilent(args...)
if !force {
return d.runSilent(args...)
}
// Send the "pull.stacked" push option on force pushes, falling back to a
// plain push for remotes that don't support push options.
withOpt := append([]string{"push", "-o", "pull.stacked"}, args[1:]...)
err := d.runSilent(withOpt...)
if err != nil && strings.Contains(err.Error(), "does not support push options") {
return d.runSilent(args...)
}
return err
}

// ResolveRemote determines the remote for pushing a branch. It checks git
Expand Down
67 changes: 67 additions & 0 deletions internal/git/gitops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ func setupBareAndClone(t *testing.T) (string, string) {
out, err := cmd.CombinedOutput()
require.NoError(t, err, "git init --bare: %s", string(out))

cfgCmd := exec.Command("git", "-c", "safe.bareRepository=all", "-C", bareDir, "config", "receive.advertisePushOptions", "true")
out, err = cfgCmd.CombinedOutput()
require.NoError(t, err, "git config: %s", string(out))

cloneDir := filepath.Join(t.TempDir(), "clone")
gitExec(t, ".", "clone", bareDir, cloneDir)

Expand Down Expand Up @@ -1703,3 +1707,66 @@ func TestIntegration_WorktreeIgnoresCallerRepositoryEnvironment(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "feature", branch)
}

// If remote supports push options, send the "pull.stacked" push option only for force pushes.
func TestIntegration_Push_SendsStackPushOption(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("uses a shell pre-receive hook")
}

for _, tc := range []struct {
name string
force, atomic bool
}{
{"force", true, false},
{"force atomic", true, true},
{"atomic", false, true},
{"plain", false, false},
} {
t.Run(tc.name, func(t *testing.T) {
bareDir, cloneDir := setupBareAndClone(t)
restore := withGitDir(t, cloneDir)
defer restore()

optsFile := filepath.Join(t.TempDir(), "opts")
hook := fmt.Sprintf("#!/bin/sh\nenv | grep '^GIT_PUSH_OPTION' | sort > %q\n", optsFile)
require.NoError(t, os.WriteFile(filepath.Join(bareDir, "hooks", "pre-receive"), []byte(hook), 0o755))

gitExec(t, cloneDir, "checkout", "-b", "b1")
writeFile(t, cloneDir, "b1.txt", "v1")
gitExec(t, cloneDir, "add", ".")
gitExec(t, cloneDir, "commit", "-m", "b1")

d := &defaultOps{}
require.NoError(t, d.Push("origin", []string{"b1"}, tc.force, tc.atomic))

got, err := os.ReadFile(optsFile)
require.NoError(t, err)
want := "GIT_PUSH_OPTION_COUNT=0\n"
if tc.force {
want = "GIT_PUSH_OPTION_0=pull.stacked\nGIT_PUSH_OPTION_COUNT=1\n"
}
assert.Equal(t, want, string(got))
})
}
}

// Remotes that don't advertise push options must still accept pushes.
func TestIntegration_Push_FallsBackWithoutPushOptionSupport(t *testing.T) {
bareDir, cloneDir := setupBareAndClone(t)
restore := withGitDir(t, cloneDir)
defer restore()

out, err := exec.Command("git", "-c", "safe.bareRepository=all", "-C", bareDir,
"config", "receive.advertisePushOptions", "false").CombinedOutput()
require.NoError(t, err, string(out))

gitExec(t, cloneDir, "checkout", "-b", "b1")
writeFile(t, cloneDir, "b1.txt", "v1")
gitExec(t, cloneDir, "add", ".")
gitExec(t, cloneDir, "commit", "-m", "b1")

d := &defaultOps{}
require.NoError(t, d.Push("origin", []string{"b1"}, true, true))
assert.Equal(t, gitExec(t, cloneDir, "rev-parse", "b1"), remoteBranchSHA(t, bareDir, "b1"))
}
Loading