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.
310327func 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.
322360func printProtectedRefBlock (w io.Writer , ref , target string ) {
323361 const banner = "[entire] ============================================================"
0 commit comments