package runner
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
)
// ForgeBranch asks the forge which branch a check-in is on
// (GET /api/v1/r/<repo>/checkin/<hash>), for a runner that runs one
// branch only. The task's own input is not asked: it came from a
// pipeline the check-in under test may have written.
type ForgeBranch struct {
API string // https://forge/api/v1/r/<repo>
// User and Password, for a private repository, as ArchiveCheckout.
User, Password string
Client *http.Client
}
func (f ForgeBranch) BranchOf(ctx context.Context, checkin string) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", strings.TrimSuffix(f.API, "/")+"/checkin/"+url.PathEscape(checkin), nil)
if err != nil {
return "", err
}
if f.User != "" {
req.SetBasicAuth(f.User, f.Password)
}
c := f.Client
if c == nil {
c = http.DefaultClient
}
resp, err := c.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("the forge answered %d for check-in %.12s", resp.StatusCode, checkin)
}
var body struct {
Hash string `json:"hash"`
Branch string `json:"branch"`
}
if err := json.NewDecoder(io.LimitReader(resp.Body, 4<<20)).Decode(&body); err != nil {
return "", fmt.Errorf("check-in %.12s: %v", checkin, err)
}
if body.Hash != checkin {
// A prefix the forge resolved, or another check-in: either way
// not an answer about the one this task names.
return "", fmt.Errorf("the forge answered for %.12s when asked about %.12s", body.Hash, checkin)
}
return body.Branch, nil
}
// Changed asks the forge for the paths checkin's branch changed since
// its fork point (GET /api/v1/r/<repo>/checkin/<hash>/diff?branch=true).
// A check-in on a branch with no fork point, trunk's own, is an error:
// there is no branch change to name.
func (f ForgeBranch) Changed(ctx context.Context, checkin string) ([]string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", strings.TrimSuffix(f.API, "/")+"/checkin/"+url.PathEscape(checkin)+"/diff?branch=true&maxLines=1", nil)
if err != nil {
return nil, err
}
if f.User != "" {
req.SetBasicAuth(f.User, f.Password)
}
c := f.Client
if c == nil {
c = http.DefaultClient
}
resp, err := c.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("the forge answered %d for the branch diff of %.12s", resp.StatusCode, checkin)
}
var body struct {
To string `json:"to"`
Files []struct {
Name string `json:"name"`
} `json:"files"`
}
if err := json.NewDecoder(io.LimitReader(resp.Body, 64<<20)).Decode(&body); err != nil {
return nil, fmt.Errorf("the branch diff of %.12s: %v", checkin, err)
}
if body.To != checkin {
return nil, fmt.Errorf("the forge answered for %.12s when asked about %.12s", body.To, checkin)
}
paths := make([]string, 0, len(body.Files))
for _, f := range body.Files {
paths = append(paths, f.Name)
}
return paths, nil
}