socle

errors.go

package main

import (
	"errors"
	"fmt"
	"strings"
)

// Error classes for the plumbing layer.
// The serve layer maps these to HTTP statuses with errors.Is:
// errInvalid means the request was malformed (400),
// errNotFound means the asked-for thing doesn't exist (404),
// and anything else is internal (500).
var (
	// errInvalid marks input rejected by a validator: a malformed repo name, ref, or tree path.
	errInvalid = errors.New("invalid")
	// errNotFound marks a lookup that came back empty: an unknown repo, an unresolvable ref, or a path absent from a tree.
	errNotFound = errors.New("not found")
)

// gitError reports a git invocation that failed.
// Read operations translate the failures that mean "doesn't exist" into errNotFound;
// a gitError that escapes to a caller is an internal problem (missing binary, repo corruption, parse surprise).
type gitError struct {
	args     []string // the git argv, excluding the leading "git" itself
	exitCode int      // the process's exit code; -1 if it never ran
	stderr   string   // trimmed stderr capture, for diagnostics
	cause    error    // the underlying exec error, if any
}

func (e *gitError) Error() string {
	msg := fmt.Sprintf("git %s: exit %d", strings.Join(e.args, " "), e.exitCode)
	if e.stderr != "" {
		msg += ": " + e.stderr
	}
	if e.cause != nil {
		msg += ": " + e.cause.Error()
	}
	return msg
}

func (e *gitError) Unwrap() error { return e.cause }

// notFoundOnExit converts a git failure into errNotFound when the process ran and exited nonzero:
// the operations that use this are ones where any clean nonzero exit means "no such ref/path".
// Failures to run at all (or errors that aren't gitError) pass through unchanged, staying internal.
func notFoundOnExit(err error, what string) error {
	var ge *gitError
	if errors.As(err, &ge) && ge.exitCode > 0 {
		return fmt.Errorf("%w: %s", errNotFound, what)
	}
	return err
}

errors.go · 1.9 KiB · c45b00c468