Skip to content

Commit 589aba5

Browse files
authored
Merge pull request #1094 from entireio/push-v2-in-parallel
Checkpoints v2: Push v2 refs in parallel and clean up output
2 parents d8f8ad6 + 04e6ad5 commit 589aba5

4 files changed

Lines changed: 406 additions & 124 deletions

File tree

cmd/entire/cli/strategy/push_common.go

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"io"
8+
"log/slog"
89
"os"
910
"os/exec"
1011
"strings"
@@ -13,6 +14,7 @@ import (
1314

1415
"github.com/entireio/cli/cmd/entire/cli/checkpoint"
1516
"github.com/entireio/cli/cmd/entire/cli/checkpoint/remote"
17+
"github.com/entireio/cli/cmd/entire/cli/logging"
1618
"github.com/entireio/cli/cmd/entire/cli/settings"
1719

1820
"github.com/go-git/go-git/v6"
@@ -284,7 +286,7 @@ func tryPushSessionsCommon(ctx context.Context, remoteName, branchName string) (
284286
result, err := remote.Push(ctx, remoteName, branchName)
285287
outputStr := result.Output
286288
if err != nil {
287-
return pushResult{}, classifyPushOutput(outputStr)
289+
return pushResult{}, classifyPushFailure(ctx, outputStr, err)
288290
}
289291

290292
return parsePushResult(outputStr), nil
@@ -306,18 +308,54 @@ func isProtectedRefRejection(output string) bool {
306308
strings.Contains(output, "protected branch hook declined")
307309
}
308310

311+
var errNonFastForward = errors.New("non-fast-forward")
312+
313+
func isNonFastForwardRejection(output string) bool {
314+
if strings.Contains(output, "non-fast-forward") {
315+
return true
316+
}
317+
for _, line := range strings.Split(output, "\n") {
318+
if strings.Contains(line, "[rejected]") && strings.Contains(line, "(fetch first)") {
319+
return true
320+
}
321+
}
322+
return strings.Contains(output, "Updates were rejected because the tip of your current branch is behind") ||
323+
strings.Contains(output, "Updates were rejected because the remote contains work that you do not have locally")
324+
}
325+
309326
// classifyPushOutput maps failing push stderr to a typed error.
310327
func classifyPushOutput(output string) error {
311328
if isProtectedRefRejection(output) {
312329
return &protectedRefError{output: output}
313330
}
314-
if strings.Contains(output, "non-fast-forward") ||
315-
strings.Contains(output, "rejected") {
316-
return errors.New("non-fast-forward")
331+
if isNonFastForwardRejection(output) {
332+
return errNonFastForward
333+
}
334+
if strings.TrimSpace(output) == "" {
335+
return errors.New("push failed")
317336
}
318337
return fmt.Errorf("push failed: %s", output)
319338
}
320339

340+
func classifyPushFailure(ctx context.Context, output string, pushErr error) error {
341+
if strings.TrimSpace(output) != "" {
342+
if pushErr != nil {
343+
logging.Debug(ctx, "git push failed",
344+
slog.String("error", pushErr.Error()),
345+
slog.String("output", output),
346+
)
347+
}
348+
return classifyPushOutput(output)
349+
}
350+
if pushErr != nil {
351+
logging.Debug(ctx, "git push failed without output",
352+
slog.String("error", pushErr.Error()),
353+
)
354+
return fmt.Errorf("push failed: %w", pushErr)
355+
}
356+
return errors.New("push failed")
357+
}
358+
321359
// printProtectedRefBlock explains that checkpoint syncing was blocked remotely.
322360
func printProtectedRefBlock(w io.Writer, ref, target string) {
323361
const banner = "[entire] ============================================================"

cmd/entire/cli/strategy/push_common_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package strategy
33
import (
44
"bytes"
55
"context"
6+
"errors"
67
"os"
78
"os/exec"
89
"path/filepath"
@@ -1538,14 +1539,41 @@ func TestClassifyPushOutput(t *testing.T) {
15381539

15391540
var perr *protectedRefError
15401541
assert.NotErrorAs(t, err, &perr)
1542+
require.ErrorIs(t, err, errNonFastForward)
15411543
assert.EqualError(t, err, "non-fast-forward")
15421544
})
15431545

1546+
t.Run("fetch-first maps to NFF error", func(t *testing.T) {
1547+
t.Parallel()
1548+
err := classifyPushOutput("!\trefs/heads/main:refs/heads/main\t[rejected] (fetch first)")
1549+
1550+
assert.ErrorIs(t, err, errNonFastForward)
1551+
})
1552+
1553+
t.Run("generic rejected output stays generic", func(t *testing.T) {
1554+
t.Parallel()
1555+
err := classifyPushOutput("remote: rejected credentials")
1556+
1557+
require.Error(t, err)
1558+
require.NotErrorIs(t, err, errNonFastForward)
1559+
assert.ErrorContains(t, err, "push failed: remote: rejected credentials")
1560+
})
1561+
15441562
t.Run("other output is wrapped as push failed", func(t *testing.T) {
15451563
t.Parallel()
15461564
err := classifyPushOutput("fatal: Could not resolve host")
15471565
assert.ErrorContains(t, err, "push failed: fatal: Could not resolve host")
15481566
})
1567+
1568+
t.Run("empty output preserves push error", func(t *testing.T) {
1569+
t.Parallel()
1570+
pushErr := errors.New("exit status 128")
1571+
err := classifyPushFailure(context.Background(), "", pushErr)
1572+
1573+
require.Error(t, err)
1574+
require.ErrorIs(t, err, pushErr)
1575+
assert.ErrorContains(t, err, "push failed")
1576+
})
15491577
}
15501578

15511579
func TestPrintProtectedRefBlock(t *testing.T) {

0 commit comments

Comments
 (0)