Fall back to the tool's own patch applier when the forge answers 500, with tests (issue 1604)

This commit is contained in:
2026-10-21 09:21:59 +00:00
parent e6053262e4
commit e5cfcb316a
3 changed files with 211 additions and 6 deletions
+72 -5
View File
@@ -326,14 +326,42 @@ func commit(ctx context.Context, f *Forge, a CommitAsk) (*Committed, error) {
var made struct {
Commit madeCommit `json:"commit"`
}
// A patch goes to the forge's diffpatch first. When the forge answers a server error (it does so, with an
// empty message, when an unchanged context line holds a non-ASCII character: novox/hq issue 1604) the tool
// applies the patch itself and commits the resulting files the way a files commit does.
files := a.Files
forgeApplied := false
if a.Patch != "" {
on["content"] = a.Patch
if err := f.SendJSON(ctx, http.MethodPost, repo+"/diffpatch", on, &made); err != nil {
err := f.SendJSON(ctx, http.MethodPost, repo+"/diffpatch", on, &made)
var status *forgehttp.StatusError
switch {
case err == nil:
forgeApplied = true
case errors.As(err, &status) && status.Status >= 500:
delete(on, "content")
read := func(p string) ([]byte, bool, error) { return f.contentAt(ctx, repo, p, ref) }
var perr error
if files, perr = patchAsFiles(a.Patch, read); perr != nil {
return nil, fmt.Errorf("the forge did not take the patch: %w; the tool could not apply it itself: %v", err, perr)
}
total := 0
for _, fc := range files {
if fc.size > maxFileBytes {
return nil, refuse("%s would be %s, more than the %s one file of a commit may be. Nothing was sent", fc.Path, mib(fc.size), mib(maxFileBytes))
}
total += fc.size
}
if total > maxCommitBytes {
return nil, refuse("the patch's files are %s together, more than the %s one commit carries. Nothing was sent", mib(total), mib(maxCommitBytes))
}
out.Said = append(out.Said, fmt.Sprintf("the forge answered %d to the patch, so the tool applied it to the files at %s and committed them", status.Status, short(ref)))
default:
return nil, fmt.Errorf("the forge did not take the patch: %w", err)
}
} else {
files := a.Files
for _, fc := range a.Files {
}
if !forgeApplied {
for _, fc := range files {
if fc.CopyFrom != nil {
// Read, and judged against the limits, before anything is sent.
var err error
@@ -391,7 +419,7 @@ func commit(ctx context.Context, f *Forge, a CommitAsk) (*Committed, error) {
moved = "base " + out.Base
}
how := "each file it changes was checked against " + short(read) + " by its blob, so none of them changed in between"
if a.Patch != "" {
if forgeApplied {
how = "the forge applied the patch there, and it refuses a patch whose context changed"
}
out.Said = append(out.Said, fmt.Sprintf("the %s moved from %s to %s between the read and the commit, so the "+
@@ -478,3 +506,42 @@ func short(sha string) string {
}
return sha
}
// contentAt is the content of a file at a ref; ok is false when there is no file there.
func (f *Forge) contentAt(ctx context.Context, repo, path, ref string) ([]byte, bool, error) {
seg, err := segments("file path", path)
if err != nil {
return nil, false, err
}
body, err := f.Do(ctx, http.MethodGet, repo+"/contents/"+seg+"?ref="+queryEscape(ref), nil, maxBody)
if err != nil {
if errors.Is(err, errNotFound) {
return nil, false, nil
}
return nil, false, err
}
var entry struct {
Type string `json:"type"`
Size int `json:"size"`
Content string `json:"content"`
Encoding string `json:"encoding"`
}
if err := jsonInto(body, &entry); err != nil {
return nil, false, fmt.Errorf("the forge's answer about %s is not readable: %w", path, err)
}
if entry.Type != "file" {
return nil, false, refuse("%s is a %s, not a file. Nothing was sent", path, entry.Type)
}
if entry.Encoding == "base64" && (entry.Content != "" || entry.Size == 0) {
raw, err := base64.StdEncoding.DecodeString(strings.ReplaceAll(entry.Content, "\n", ""))
if err != nil {
return nil, false, fmt.Errorf("the content of %s is not base64: %w", path, err)
}
return raw, true, nil
}
raw, err := f.Do(ctx, http.MethodGet, repo+"/raw/"+seg+"?ref="+queryEscape(ref), nil, maxFileBytes+1)
if err != nil {
return nil, false, err
}
return raw, true, nil
}