Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5b22ab2
test: specify the ToDo3463 todo update reader before implementing it
mashfromband Oct 23, 2026
76fdc92
feat: qualify the one ToDo3190 todo.exe update with an independent re…
mashfromband Oct 23, 2026
96709e0
Merge remote-tracking branch 'origin/main' into scheduler/todo-3463-t…
mashfromband Oct 23, 2026
d9edfd4
ci: test the ToDo3190 todo update reader on the Windows runner
mashfromband Oct 23, 2026
dd4b027
test: require the ToDo3190 reader to keep Git off working files
mashfromband Oct 23, 2026
74c87e3
fix: keep Git off working files in the ToDo3190 reader
mashfromband Oct 23, 2026
308cb24
docs: record the ToDo3482 review and the measured Must fix
mashfromband Oct 23, 2026
6bda9f6
test: require Git to read only objects in the ToDo3190 reader
mashfromband Oct 23, 2026
2f804f1
fix: let Git read only the object store in the ToDo3190 reader
mashfromband Oct 23, 2026
8b4f34b
docs: record the ToDo3491 review and the isolated Git fix
mashfromband Oct 23, 2026
3b9b98d
test: require the ToDo3190 reader to read objects and packed-refs as …
mashfromband Oct 23, 2026
fe1d594
fix: read objects without Git and packed-refs as Git does in the ToDo…
mashfromband Oct 23, 2026
206181d
docs: record the ToDo3505 review and the reader that runs no Git
mashfromband Oct 23, 2026
9fb6c2b
test: require the ToDo3190 reader to read commits as Git does and bou…
mashfromband Oct 23, 2026
7a008ac
fix: read commits as Git does and bound the object reads in the ToDo3…
mashfromband Oct 23, 2026
b3c0b71
docs: record the ToDo3513 review and the bounded object reads
mashfromband Oct 23, 2026
edbf962
test: require the ToDo3190 reader to read every commit Git reads and …
mashfromband Oct 23, 2026
c2ff858
fix: read every commit Git reads and keep one type per name in the To…
mashfromband Oct 23, 2026
4ed8315
docs: record the ToDo3531 review and the reader that reads every comm…
mashfromband Oct 23, 2026
59414fc
test: keep Git's automatic maintenance out of the ToDo3190 reader's f…
mashfromband Oct 23, 2026
64b28eb
test: require the ToDo3190 reader to give shallow names the commit ty…
mashfromband Oct 23, 2026
bb3bb1b
fix: give every shallow name the commit type in the ToDo3190 reader, …
mashfromband Oct 23, 2026
d30bc92
docs: record the CI failure from Git's maintenance, review 3545 and t…
mashfromband Oct 23, 2026
3cd052b
test: start Git for the config listing through gitCommand
mashfromband Oct 24, 2026
0a63ca8
docs: record review 3548, its suggestion and the passing CI of d30bc929
mashfromband Oct 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/todo-update-reader-3190.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: ToDo3190 todo update reader
on:
push:
branches: [main]
paths: ['tools/todo-update-reader-3190/**', '.github/workflows/todo-update-reader-3190.yml']
pull_request:
paths: ['tools/todo-update-reader-3190/**', '.github/workflows/todo-update-reader-3190.yml']
permissions:
contents: read
# Superseded PR heads need no second verification; preserve every main run.
concurrency:
group: ${{ github.workflow }}-pr-head-v2-${{ github.event_name == 'pull_request' && github.run_attempt == 1 && github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' && github.run_attempt == 1 }}
jobs:
windows:
if: github.repository == 'AutoDevJapan/cc-pc-tools-rs' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
# The reader uses Windows handle, link and final-path semantics, and its
# synthetic Git repositories, hard links and junctions need the PC runner.
runs-on: [self-hosted, Windows, X64, cc-pc-tools-windows]
timeout-minutes: 20
defaults:
run:
working-directory: tools/todo-update-reader-3190
steps:
- name: Admit current Windows source before checkout
env:
CI_JOB_TOKEN: ${{ github.token }}
working-directory: ${{ runner.temp }}
shell: pwsh
run: |
& $env:PC_CI_ADMISSION admit
exit $LASTEXITCODE
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
- uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: '1.27.x'
cache: false
- run: go test -race -timeout 900s ./...
- run: go vet ./...
- run: go build -trimpath .
552 changes: 552 additions & 0 deletions docs/todo-update-reader-3190.md

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions tools/todo-update-reader-3190/checkout_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package main

import (
"bytes"
"crypto/sha1"
"encoding/hex"
"strconv"
)

// objectName is the SHA-1 name Git gives an object of kind with data.
func objectName(kind string, data []byte) string {
h := sha1.New()
h.Write([]byte(kind + " " + strconv.Itoa(len(data)) + "\x00"))
h.Write(data)
return hex.EncodeToString(h.Sum(nil))
}

// gitBlobID is the name Git gives data stored as a blob. It ties bytes to an
// entry of the fixed tree; SHA-256 pins the same bytes separately.
func gitBlobID(data []byte) string { return objectName("blob", data) }

// checkedOut reports whether working bytes are exactly a fixed blob as Git
// writes it out on this PC: the blob itself or, under core.autocrlf=true, the
// blob with every LF written as CRLF. Git converts only text, so bytes with a
// NUL, a lone CR or a lone LF are never accepted as a conversion. Only the
// reader's pinned bytes are hashed; Git never opens the working file.
func checkedOut(data []byte, blob string) bool {
if gitBlobID(data) == blob {
return true
}
pairs := bytes.Count(data, []byte("\r\n"))
if pairs == 0 || bytes.IndexByte(data, 0) >= 0 ||
bytes.Count(data, []byte("\r")) != pairs || bytes.Count(data, []byte("\n")) != pairs {
return false
}
return gitBlobID(bytes.ReplaceAll(data, []byte("\r\n"), []byte("\n"))) == blob
}
112 changes: 112 additions & 0 deletions tools/todo-update-reader-3190/cli_windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package main

import (
"encoding/json"
"sort"
"strings"
"testing"
)

const cliTarget = `D:\CC\Tools\todo\todo.exe`

func TestCLIAcceptsOnlyThePurposeFlag(t *testing.T) {
for _, args := range [][]string{
{"--verify-todo-runtime-update", cliTarget},
{"-verify-todo-runtime-update", cliTarget},
{"--verify-todo-runtime-update=" + cliTarget},
{"-verify-todo-runtime-update=" + cliTarget},
} {
called := ""
var out, errOut strings.Builder
code := runCLI(args, &out, &errOut, func(path string) (qualification, string) {
called = path
return qualification{}, "baseline_mismatch"
})
if called != cliTarget || code != 1 || out.String() != "{\"qualified\":false}\n" {
t.Errorf("%q: called %q code %d stdout %q", args, called, code, out.String())
}
}
for _, args := range [][]string{
{"--verify-todo-runtime-update"},
{"--verify-todo-runtime-update", "todo.exe"},
{"--verify-todo-runtime-update", `tools\todo\todo.exe`},
{"--verify-todo-runtime-update", cliTarget, "extra"},
{"--verify-todo-runtime-update", cliTarget, "--verify-todo-runtime-update", cliTarget},
{"--manifest", "m.json", "--verify-todo-runtime-update", cliTarget},
{"--verify-todo-runtime-update", cliTarget, "--sha256", strings.Repeat("0", 64)},
{"--guard-path", cliTarget},
{"--verify-capacity-runtime-update", cliTarget},
{"--verify-model-runtime-update", cliTarget},
{"--verify-todo-runtime-update="},
{"--verify-todo-runtime-update=todo.exe"},
{"verify-todo-runtime-update", cliTarget},
{"---verify-todo-runtime-update", cliTarget},
{"--Verify-Todo-Runtime-Update", cliTarget},
{"--help", "--verify-todo-runtime-update", cliTarget},
} {
called := false
var out, errOut strings.Builder
code := runCLI(args, &out, &errOut, func(string) (qualification, string) {
called = true
return qualification{Qualified: true}, ""
})
if called || code != 1 || out.String() != "{\"qualified\":false}\n" || !strings.Contains(errOut.String(), "argument") {
t.Errorf("%q: called %v code %d stdout %q stderr %q", args, called, code, out.String(), errOut.String())
}
}
}

func TestCLIHelpAndUsage(t *testing.T) {
for _, args := range [][]string{{"-h"}, {"--help"}, {"-help"}} {
var out, errOut strings.Builder
code := runCLI(args, &out, &errOut, func(string) (qualification, string) {
t.Fatal("help must not verify")
return qualification{}, ""
})
if code != 0 || out.Len() != 0 || !strings.Contains(errOut.String(), "-verify-todo-runtime-update") {
t.Errorf("%q: code %d stdout %q stderr %q", args, code, out.String(), errOut.String())
}
}
var out, errOut strings.Builder
if code := runCLI(nil, &out, &errOut, nil); code != 2 || out.Len() != 0 || !strings.Contains(errOut.String(), "Usage") {
t.Errorf("no arguments: code %d stdout %q stderr %q", code, out.String(), errOut.String())
}
}

func TestCLIPrintsOnlyTheFixedQualification(t *testing.T) {
var out, errOut strings.Builder
code := runCLI([]string{"--verify-todo-runtime-update", cliTarget}, &out, &errOut, func(string) (qualification, string) {
return qualification{Qualified: true, Domain: "todo-runtime-update-3190-v1", TodoID: 3190, VerifierTodoID: 3463, TargetKind: "todo", BuildCommand: []string{"cargo"}}, ""
})
if code != 0 || errOut.Len() != 0 {
t.Fatalf("code %d stderr %q", code, errOut.String())
}
var got map[string]any
if err := json.Unmarshal([]byte(out.String()), &got); err != nil {
t.Fatal(err)
}
var keys []string
for k := range got {
keys = append(keys, k)
}
sort.Strings(keys)
want := "baseline_qualified_by baseline_source_receipt_sha256 build_command build_job build_log_sha256 build_receipt_sha256 current_sha256 data_access_granted dep_info_entries dep_info_sha256 domain inputs_verified local_crates new_bytes new_sha256 original_registry_recovered qualified source_head source_repository source_root source_tree target target_kind todo_id verifier_todo_id"
if strings.Join(keys, " ") != want {
t.Fatalf("keys %s\nwant %s", strings.Join(keys, " "), want)
}
if got["data_access_granted"] != false || got["qualified"] != true {
t.Fatalf("output %v", got)
}
}

func TestCLIHidesRejectionDetails(t *testing.T) {
var out, errOut strings.Builder
leak := strings.Repeat("d", 64)
code := runCLI([]string{"--verify-todo-runtime-update", cliTarget}, &out, &errOut, func(string) (qualification, string) {
return qualification{CurrentSHA256: leak, SourceRoot: "D:/private"}, "baseline_mismatch"
})
if code != 1 || out.String() != "{\"qualified\":false}\n" || !strings.Contains(errOut.String(), "baseline_mismatch") ||
strings.Contains(out.String()+errOut.String(), leak) || strings.Contains(out.String()+errOut.String(), "private") {
t.Fatalf("code %d stdout %q stderr %q", code, out.String(), errOut.String())
}
}
114 changes: 114 additions & 0 deletions tools/todo-update-reader-3190/commit_parse_windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package main

import (
"bytes"
"compress/zlib"
"errors"
"os"
"os/exec"
"path/filepath"
"slices"
"strconv"
"strings"
"testing"
)

// writeLooseObject stores body as a loose object of kind, compressed as Git
// stores one, and returns the object's name.
func writeLooseObject(t *testing.T, objects, kind, body string) string {
t.Helper()
id := objectName(kind, []byte(body))
var buf bytes.Buffer
z := zlib.NewWriter(&buf)
if _, err := z.Write([]byte(kind + " " + strconv.Itoa(len(body)) + "\x00" + body)); err != nil {
t.Fatal(err)
}
if err := z.Close(); err != nil {
t.Fatal(err)
}
writeFile(t, filepath.Join(objects, id[:2], id[2:]), buf.String())
return id
}

// gitCommitView returns the tree and the parents that Git reads from the
// commit id, or false if Git refuses to read the commit.
func gitCommitView(t *testing.T, dir, id string) (string, []string, bool) {
t.Helper()
out, err := testGit(t, dir, "log", "--no-walk", "--format=%T %P", id)
if err != nil {
return "", nil, false
}
fields := strings.Fields(out)
if len(fields) == 0 {
t.Fatalf("git log printed %q", out)
}
return fields[0], fields[1:], true
}

// Git's parse_commit_buffer reads a "tree " line that more bytes follow, then
// takes a line that starts with "parent " as a parent only while more than
// such a line's length follows it, and refuses the commit if that line is not
// "parent <id>\n" with more bytes after it. The reader refuses each commit
// that Git refuses, and also each commit whose "parent " line Git would not
// read, so every commit it accepts has the tree and parents Git reads.
func TestParseCommitAgreesWithGit(t *testing.T) {
const (
tree = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
p1 = "1111111111111111111111111111111111111111"
p2 = "2222222222222222222222222222222222222222"
who = "author A <a@example.invalid> 1700000000 +0000\ncommitter C <c@example.invalid> 1700000001 +0000\n"
)
refused := []string(nil)
cases := []struct {
name string
body string
git, reader []string // the parents read; nil if the commit is refused
}{
{"root commit", "tree " + tree + "\n" + who + "\nmessage\n", []string{}, []string{}},
{"two parents", "tree " + tree + "\nparent " + p1 + "\nparent " + p2 + "\n" + who + "\nmessage\n", []string{p1, p2}, []string{p1, p2}},
{"parent line in the message", "tree " + tree + "\n\nparent " + p1 + "\n", []string{}, []string{}},
{"tree line only", "tree " + tree + "\n", refused, refused},
{"tree line without its newline", "tree " + tree, refused, refused},
{"parent line without its newline", "tree " + tree + "\nparent " + p1, []string{}, refused},
{"commit ends with a parent line", "tree " + tree + "\nparent " + p1 + "\n", refused, refused},
{"second parent line without its newline", "tree " + tree + "\nparent " + p1 + "\nparent " + p2, []string{p1}, refused},
{"short parent", "tree " + tree + "\nparent 1111\n" + who, refused, refused},
{"long parent", "tree " + tree + "\nparent " + p1 + "1\n" + who, refused, refused},
{"upper-case tree", "tree " + strings.ToUpper(tree) + "\n" + who, []string{}, refused},
}
dir := filepath.Join(testLongPath(t, t.TempDir()), "repo")
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
if out, err := testGit(t, dir, "init", "-q"); err != nil {
t.Fatalf("git init: %v\n%s", err, out)
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
id := writeLooseObject(t, filepath.Join(dir, ".git", "objects"), "commit", tc.body)
gitTree, gitParents, gitRead := gitCommitView(t, dir, id)
if gitRead != (tc.git != nil) || gitRead && (gitTree != tree || !slices.Equal(gitParents, tc.git)) {
t.Fatalf("Git read tree %q parents %q (%v), want parents %q", gitTree, gitParents, gitRead, tc.git)
}
gotTree, gotParents, ok := parseCommit([]byte(tc.body))
if ok != (tc.reader != nil) || ok && (gotTree != gitTree || !slices.Equal(gotParents, tc.reader)) {
t.Fatalf("parseCommit read tree %q parents %q (%v), want parents %q", gotTree, gotParents, ok, tc.reader)
}
})
}
}

// A commit that ends in a "parent " line without its newline has no parent to
// Git. Made the tip of main, it must not let the reader find the fixed commit
// behind it where Git finds no ancestry.
func TestRefusesParentsGitDoesNotRead(t *testing.T) {
f := newFixture(t, nil)
id := writeLooseObject(t, filepath.Join(f.common, "objects"), "commit", "tree "+f.p.tree+"\nparent "+f.p.head)
writeFile(t, filepath.Join(f.common, "refs", "remotes", "origin", "main"), id+"\n")
_, err := testGit(t, f.root, "merge-base", "--is-ancestor", f.p.head, id)
var exit *exec.ExitError
if !errors.As(err, &exit) || exit.ExitCode() != 1 {
t.Fatalf("git merge-base --is-ancestor %s %s: %v, want exit status 1", f.p.head, id, err)
}
expectReason(t, f, "source_git")
}
Loading
Loading