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
}
// Touched asks the forge for the paths checkin itself changed against
// its first parent (GET /api/v1/r/<repo>/checkin/<hash>/diff): for a
// landing's merge onto trunk, everything it landed.
func (f ForgeBranch) Touched(ctx context.Context, checkin string) ([]string, error) {
return f.diffFiles(ctx, checkin, "?maxLines=1")
}
// 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) {
return f.diffFiles(ctx, checkin, "?branch=true&maxLines=1")
}
// diffFiles reads the file list of one of checkin's diffs.
func (f ForgeBranch) diffFiles(ctx context.Context, checkin, query string) ([]string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", strings.TrimSuffix(f.API, "/")+"/checkin/"+url.PathEscape(checkin)+"/diff"+query, 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 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 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
}