package builder import ( "bytes" "context" "errors" "fmt" "os" "os/exec" "path/filepath" "strings" ) // **The base branch's check judges a pull request** (novox/hq issue 310). // // A repository's own check is its merge-check.sh, the parts it declares (`# mesh-check-also:`) and the // toolchain it names. Were the check the one in the pull request's head, a change could delete or weaken // the script and so skip its own tests — and a branch cut before the repository had a script escaped // its tests without deleting anything. So when the base branch holds a merge-check.sh, **the base // branch's** script, its declared parts and its toolchain run against the change's tree, and the verdict // says so whenever the change lacks the script or alters any of it. // // A change that legitimately alters the check is judged by the check it replaces: its own version judges // the pull requests after it merges. A base branch with no script keeps what was so before — the head's, // when it brings one, and a warning when it brings none. // Judging is the check that judges a change, and what of it the base branch puts in the change's tree. type Judging struct { // Script is the merge-check.sh that judges, nil when neither the base branch nor the change holds one. Script []byte // Said is what the verdict says of it, empty when the change's own script runs as the base's would. Said string // files are the base branch's scripts, by path in the tree, put there before the check runs. files map[string][]byte } // Put writes the base branch's scripts into the change's tree, in place of the change's own. func (j Judging) Put(tree string) error { for path, body := range j.files { at := filepath.Join(tree, path) if err := os.MkdirAll(filepath.Dir(at), 0o755); err != nil { return err } if err := os.WriteFile(at, body, 0o755); err != nil { return fmt.Errorf("the base branch's %s cannot be put in the change's tree: %w", path, err) } } return nil } // TheCheckThatJudges is the check that judges the change cloned at tree, merging into base: the base // branch's when it holds one, the change's own otherwise. An error is that the base branch could not be // read — never a reason to fall back to the change's script. func TheCheckThatJudges(ctx context.Context, tree, base string) (Judging, error) { head, err := os.ReadFile(filepath.Join(tree, CheckScript)) if err != nil && !errors.Is(err, os.ErrNotExist) { return Judging{}, err } if err != nil { head = nil } if base == "" { return Judging{Script: head}, nil } ref := "origin/" + base if _, err := gitIn(ctx, tree, "rev-parse", "--verify", "--quiet", ref+"^{commit}"); err != nil { return Judging{}, fmt.Errorf("the base branch %s cannot be read, and the check it holds judges the change: %w", base, err) } script, held, err := onBranch(ctx, tree, ref, CheckScript) if err != nil { return Judging{}, err } if !held { return Judging{Script: head}, nil } j := Judging{Script: script, files: map[string][]byte{CheckScript: script}} var altered []string if head != nil && !bytes.Equal(head, script) { altered = append(altered, CheckScript) } for _, part := range ScriptParts(script)[1:] { if !filepath.IsLocal(part.Script) { continue // refused by name when the check runs } body, held, err := onBranch(ctx, tree, ref, part.Script) if err != nil { return Judging{}, err } if !held { continue // the base branch's own check lacks it: the change's, if it brings it } j.files[filepath.Clean(part.Script)] = body if own, err := os.ReadFile(filepath.Join(tree, part.Script)); err != nil || !bytes.Equal(own, body) { altered = append(altered, part.Script) } } switch { case head == nil: j.Said = fmt.Sprintf("the change holds no %s: %s's judged it", CheckScript, base) if len(altered) > 0 { j.Said += fmt.Sprintf(", and the change alters its part(s) %s", strings.Join(altered, ", ")) } case len(altered) > 0: j.Said = fmt.Sprintf("THE CHANGE ALTERS ITS OWN CHECK (%s): %s's version judged it; the change's judges "+ "the pull requests after it merges", strings.Join(altered, ", "), base) } return j, nil } // onBranch is a file as a ref has it, and whether the ref holds it at all. func onBranch(ctx context.Context, tree, ref, path string) ([]byte, bool, error) { at := ref + ":" + filepath.ToSlash(filepath.Clean(path)) // ls-tree answers nothing for a path the ref does not hold, and fails only when it cannot read. listed, err := gitIn(ctx, tree, "ls-tree", ref, "--", filepath.ToSlash(filepath.Clean(path))) if err != nil { return nil, false, fmt.Errorf("%s cannot be read: %w", ref, err) } if fields := strings.Fields(string(listed)); len(fields) < 2 || fields[1] != "blob" { return nil, false, nil // the ref holds no such file } body, err := gitIn(ctx, tree, "cat-file", "blob", at) if err != nil { return nil, false, fmt.Errorf("%s cannot be read: %w", at, err) } return body, true, nil } func gitIn(ctx context.Context, dir string, args ...string) ([]byte, error) { cmd := exec.CommandContext(ctx, "git", args...) cmd.Dir = dir return cmd.Output() }