Read files from captured Git commits
This commit is contained in:
@@ -40,13 +40,13 @@ these implemented packages:
|
|||||||
| `internal/build` | Candidate-build coordination, selected Go target compilation, version injection, native smoke check, and artifact digests. |
|
| `internal/build` | Candidate-build coordination, selected Go target compilation, version injection, native smoke check, and artifact digests. |
|
||||||
| `internal/candidate` | Stable-version, note, repository-state, candidate capture, and post-validation checks. |
|
| `internal/candidate` | Stable-version, note, repository-state, candidate capture, and post-validation checks. |
|
||||||
| `internal/delivery` | Tagged-source and release-note checks, managed builds in disposable storage outside the checkout, prebuilt artifact validation, manifest assembly, and hosted delivery coordination. |
|
| `internal/delivery` | Tagged-source and release-note checks, managed builds in disposable storage outside the checkout, prebuilt artifact validation, manifest assembly, and hosted delivery coordination. |
|
||||||
| `internal/git` | Git command boundary, push-destination resolution, exact ref inspection, tagged checkout preparation, and tag operations, including captured-endpoint verification during recovery. |
|
| `internal/git` | Git command boundary, committed-tree and blob reads, push-destination resolution, exact ref inspection, tagged checkout preparation, and tag operations, including captured-endpoint verification during recovery. |
|
||||||
| `internal/gitea` | Gitea API transport, hosted release pages, asset inspection, manifests, and uploads. |
|
| `internal/gitea` | Gitea API transport, hosted release pages, asset inspection, manifests, and uploads. |
|
||||||
| `internal/gotool` | Effective Go configuration discovery, shared native-validation and static-build environments, and network-capable dependency preparation; see [Go command environments](gotool.md). |
|
| `internal/gotool` | Effective Go configuration discovery, shared native-validation and static-build environments, and network-capable dependency preparation; see [Go command environments](gotool.md). |
|
||||||
| `internal/tempdir` | Disposable system temporary storage outside source worktrees. |
|
| `internal/tempdir` | Disposable system temporary storage outside source worktrees. |
|
||||||
| `internal/process` | Cancellable child-process execution with per-command environment overlays and removals. |
|
| `internal/process` | Cancellable child-process execution with per-command environment overlays and removals. |
|
||||||
| `internal/preparation` | Coordination for expected-commit tag checkout and Go module dependency preparation. |
|
| `internal/preparation` | Coordination for expected-commit tag checkout and Go module dependency preparation. |
|
||||||
| `internal/validation` | Shared default Go validation and structured check failures for captured candidates; `internal/candidate` retains capture, callback sequencing, and local-state rechecks. |
|
| `internal/validation` | Shared default Go validation, structured check failures, and lazy committed-file access for captured candidates; `internal/candidate` retains capture, callback sequencing, and local-state rechecks. |
|
||||||
| `internal/publish` | Publication guards, one validation run, annotated tag creation, push, and recovery state. |
|
| `internal/publish` | Publication guards, one validation run, annotated tag creation, push, and recovery state. |
|
||||||
| `internal/runtimeinfo` | Private version lookup used by the runtime facade. |
|
| `internal/runtimeinfo` | Private version lookup used by the runtime facade. |
|
||||||
| `internal/versioncontract` | Shared stable-version rules and linker-symbol identity. |
|
| `internal/versioncontract` | Shared stable-version rules and linker-symbol identity. |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Validation model implementation plan
|
# Validation model implementation plan
|
||||||
|
|
||||||
Status: In progress; the shared validation executor and internal diagnostics are complete.
|
Status: In progress; the shared executor, internal diagnostics, and committed-file reader are complete.
|
||||||
|
|
||||||
This plan implements the enabling work in [model.md](model.md), guided by
|
This plan implements the enabling work in [model.md](model.md), guided by
|
||||||
[ADR 0003](../adr/0003-declarative-validation.md). It is written for a Luna
|
[ADR 0003](../adr/0003-declarative-validation.md). It is written for a Luna
|
||||||
@@ -229,6 +229,10 @@ complete error text.
|
|||||||
|
|
||||||
## Stage 3: Add committed candidate-file enumeration
|
## Stage 3: Add committed candidate-file enumeration
|
||||||
|
|
||||||
|
**Completion:** Complete. `internal/git` enumerates committed tree entries and
|
||||||
|
blob sizes, while `internal/validation` exposes a lazy candidate-tree reader
|
||||||
|
that serves blob bytes by object ID.
|
||||||
|
|
||||||
**Outcome:** File-oriented validators can share an offline reader bound to the
|
**Outcome:** File-oriented validators can share an offline reader bound to the
|
||||||
captured commit.
|
captured commit.
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,26 @@
|
|||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
"path"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/process"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TreeEntry records one committed path and its Git object metadata.
|
||||||
|
type TreeEntry struct {
|
||||||
|
Path string
|
||||||
|
ObjectID string
|
||||||
|
Mode string
|
||||||
|
Type string
|
||||||
|
Size int64
|
||||||
|
HasSize bool
|
||||||
|
}
|
||||||
|
|
||||||
type TaggedSource struct {
|
type TaggedSource struct {
|
||||||
Head string
|
Head string
|
||||||
Status string
|
Status string
|
||||||
@@ -74,3 +88,112 @@ func ReadCommitFile(ctx context.Context, repositoryDir, commit, name string) ([]
|
|||||||
}
|
}
|
||||||
return []byte(content), nil
|
return []byte(content), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReadTree enumerates all entries in a commit and records blob sizes. Paths
|
||||||
|
// are NUL-delimited by Git so whitespace in a filename is preserved.
|
||||||
|
func ReadTree(ctx context.Context, repositoryDir, commit string) ([]TreeEntry, error) {
|
||||||
|
if !validObjectID(commit) {
|
||||||
|
return nil, fmt.Errorf("invalid commit object ID %q", commit)
|
||||||
|
}
|
||||||
|
output, err := run(ctx, repositoryDir, nil, "ls-tree", "-r", "-z", "--full-tree", commit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("enumerate committed tree %s: %w", commit, err)
|
||||||
|
}
|
||||||
|
entries, err := parseTreeEntries([]byte(output))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse committed tree %s: %w", commit, err)
|
||||||
|
}
|
||||||
|
if err := populateBlobSizes(ctx, repositoryDir, entries); err != nil {
|
||||||
|
return nil, fmt.Errorf("read committed blob sizes for %s: %w", commit, err)
|
||||||
|
}
|
||||||
|
return entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadBlob reads the bytes for a Git blob object ID without consulting the
|
||||||
|
// worktree.
|
||||||
|
func ReadBlob(ctx context.Context, repositoryDir, objectID string) ([]byte, error) {
|
||||||
|
if !validObjectID(objectID) {
|
||||||
|
return nil, fmt.Errorf("invalid blob object ID %q", objectID)
|
||||||
|
}
|
||||||
|
content, err := run(ctx, repositoryDir, nil, "cat-file", "blob", objectID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read blob %s: %w", objectID, err)
|
||||||
|
}
|
||||||
|
return []byte(content), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTreeEntries(output []byte) ([]TreeEntry, error) {
|
||||||
|
if len(output) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
var entries []TreeEntry
|
||||||
|
for _, record := range bytes.Split(output, []byte{0}) {
|
||||||
|
if len(record) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tab := bytes.IndexByte(record, '\t')
|
||||||
|
if tab < 0 {
|
||||||
|
return nil, fmt.Errorf("tree record has no path separator")
|
||||||
|
}
|
||||||
|
fields := strings.Fields(string(record[:tab]))
|
||||||
|
if len(fields) != 3 || !validObjectID(fields[2]) {
|
||||||
|
return nil, fmt.Errorf("invalid tree record metadata %q", record[:tab])
|
||||||
|
}
|
||||||
|
entries = append(entries, TreeEntry{
|
||||||
|
Mode: fields[0],
|
||||||
|
Type: fields[1],
|
||||||
|
ObjectID: fields[2],
|
||||||
|
Path: string(record[tab+1:]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func populateBlobSizes(ctx context.Context, repositoryDir string, entries []TreeEntry) error {
|
||||||
|
var blobIndexes []int
|
||||||
|
var input strings.Builder
|
||||||
|
for index := range entries {
|
||||||
|
if entries[index].Type != "blob" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
blobIndexes = append(blobIndexes, index)
|
||||||
|
input.WriteString(entries[index].ObjectID)
|
||||||
|
input.WriteByte('\n')
|
||||||
|
}
|
||||||
|
if len(blobIndexes) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
err := process.Run(ctx, process.Invocation{
|
||||||
|
Args: []string{"git", "cat-file", "--batch-check=%(objectname) %(objecttype) %(objectsize)"},
|
||||||
|
Dir: repositoryDir,
|
||||||
|
Stdin: strings.NewReader(input.String()),
|
||||||
|
Stdout: &stdout,
|
||||||
|
Stderr: &stderr,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
message := strings.TrimSpace(stderr.String())
|
||||||
|
if message != "" {
|
||||||
|
return fmt.Errorf("git cat-file --batch-check: %s: %w", message, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
lines := strings.Split(strings.TrimSuffix(stdout.String(), "\n"), "\n")
|
||||||
|
if len(lines) != len(blobIndexes) {
|
||||||
|
return fmt.Errorf("git returned %d blob sizes for %d blobs", len(lines), len(blobIndexes))
|
||||||
|
}
|
||||||
|
for index, line := range lines {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
entry := &entries[blobIndexes[index]]
|
||||||
|
if len(fields) != 3 || fields[0] != entry.ObjectID || fields[1] != "blob" {
|
||||||
|
return fmt.Errorf("unexpected blob size record %q", line)
|
||||||
|
}
|
||||||
|
size, err := strconv.ParseInt(fields[2], 10, 64)
|
||||||
|
if err != nil || size < 0 {
|
||||||
|
return fmt.Errorf("invalid blob size in record %q", line)
|
||||||
|
}
|
||||||
|
entry.Size = size
|
||||||
|
entry.HasSize = true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type Invocation struct {
|
|||||||
Dir string
|
Dir string
|
||||||
Env map[string]string
|
Env map[string]string
|
||||||
UnsetEnv []string
|
UnsetEnv []string
|
||||||
|
Stdin io.Reader
|
||||||
Stdout io.Writer
|
Stdout io.Writer
|
||||||
Stderr io.Writer
|
Stderr io.Writer
|
||||||
}
|
}
|
||||||
@@ -32,6 +33,7 @@ func Run(ctx context.Context, invocation Invocation) error {
|
|||||||
|
|
||||||
command := exec.CommandContext(ctx, invocation.Args[0], invocation.Args[1:]...)
|
command := exec.CommandContext(ctx, invocation.Args[0], invocation.Args[1:]...)
|
||||||
command.Dir = invocation.Dir
|
command.Dir = invocation.Dir
|
||||||
|
command.Stdin = invocation.Stdin
|
||||||
if len(invocation.Env) != 0 || len(invocation.UnsetEnv) != 0 {
|
if len(invocation.Env) != 0 || len(invocation.UnsetEnv) != 0 {
|
||||||
command.Env = environmentEntries(invocation.Env, invocation.UnsetEnv)
|
command.Env = environmentEntries(invocation.Env, invocation.UnsetEnv)
|
||||||
}
|
}
|
||||||
|
|||||||
72
internal/validation/tree.go
Normal file
72
internal/validation/tree.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CandidateTree is an inventory of one captured commit with blob contents
|
||||||
|
// loaded lazily by object ID.
|
||||||
|
type CandidateTree struct {
|
||||||
|
repositoryDir string
|
||||||
|
entries []git.TreeEntry
|
||||||
|
byPath map[string]int
|
||||||
|
blobs map[string][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadCandidateTree enumerates the captured commit without reading from the
|
||||||
|
// worktree.
|
||||||
|
func ReadCandidateTree(ctx context.Context, repositoryDir, commit string) (*CandidateTree, error) {
|
||||||
|
entries, err := git.ReadTree(ctx, repositoryDir, commit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tree := &CandidateTree{
|
||||||
|
repositoryDir: repositoryDir,
|
||||||
|
entries: entries,
|
||||||
|
byPath: make(map[string]int, len(entries)),
|
||||||
|
blobs: make(map[string][]byte),
|
||||||
|
}
|
||||||
|
for index, entry := range entries {
|
||||||
|
tree.byPath[entry.Path] = index
|
||||||
|
}
|
||||||
|
return tree, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entries returns the committed tree metadata in Git's tree order.
|
||||||
|
func (tree *CandidateTree) Entries() []git.TreeEntry {
|
||||||
|
return append([]git.TreeEntry(nil), tree.entries...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadFile returns the committed blob bytes for path. Contents are cached by
|
||||||
|
// object ID for later reads from this tree.
|
||||||
|
func (tree *CandidateTree) ReadFile(ctx context.Context, path string) ([]byte, error) {
|
||||||
|
index, ok := tree.byPath[path]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("committed path %q does not exist", path)
|
||||||
|
}
|
||||||
|
entry := tree.entries[index]
|
||||||
|
if entry.Type != "blob" {
|
||||||
|
return nil, fmt.Errorf("committed path %q is a %s entry, not a blob", path, entry.Type)
|
||||||
|
}
|
||||||
|
if contents, ok := tree.blobs[entry.ObjectID]; ok {
|
||||||
|
return append([]byte(nil), contents...), nil
|
||||||
|
}
|
||||||
|
contents, err := git.ReadBlob(ctx, tree.repositoryDir, entry.ObjectID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read committed path %q: %w", path, err)
|
||||||
|
}
|
||||||
|
tree.blobs[entry.ObjectID] = append([]byte(nil), contents...)
|
||||||
|
return contents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entry returns the committed metadata for path when present.
|
||||||
|
func (tree *CandidateTree) Entry(path string) (git.TreeEntry, bool) {
|
||||||
|
index, ok := tree.byPath[path]
|
||||||
|
if !ok {
|
||||||
|
return git.TreeEntry{}, false
|
||||||
|
}
|
||||||
|
return tree.entries[index], true
|
||||||
|
}
|
||||||
125
internal/validation/tree_test.go
Normal file
125
internal/validation/tree_test.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReadCandidateTreeUsesCapturedCommitAndEnumeratesGitEntries(t *testing.T) {
|
||||||
|
repository := newTreeRepository(t)
|
||||||
|
writeTreeFile(t, repository, "README.md", []byte("captured\n"))
|
||||||
|
writeTreeFile(t, repository, "vendor/library/go.mod", []byte("module vendor.example\n"))
|
||||||
|
writeTreeFile(t, repository, "nested/module/go.mod", []byte("module nested.example\n"))
|
||||||
|
writeTreeFile(t, repository, "binary.dat", []byte{0, 1, 255, 2})
|
||||||
|
unusualPath := "space tab\tline\nname.txt"
|
||||||
|
writeTreeFile(t, repository, unusualPath, []byte("unusual path\n"))
|
||||||
|
if err := os.Symlink("README.md", filepath.Join(repository, "readme-link")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
submoduleCommit := newTreeRepository(t)
|
||||||
|
writeTreeFile(t, submoduleCommit, "sub.txt", []byte("submodule\n"))
|
||||||
|
gitTree(t, submoduleCommit, "add", "--all")
|
||||||
|
gitTree(t, submoduleCommit, "commit", "-m", "create submodule source")
|
||||||
|
submoduleOID := strings.TrimSpace(gitTree(t, submoduleCommit, "rev-parse", "HEAD"))
|
||||||
|
gitTree(t, repository, "add", "--all")
|
||||||
|
gitTree(t, repository, "update-index", "--add", "--cacheinfo", "160000,"+submoduleOID+",submodule")
|
||||||
|
gitTree(t, repository, "commit", "-m", "capture candidate files")
|
||||||
|
commit := strings.TrimSpace(gitTree(t, repository, "rev-parse", "HEAD"))
|
||||||
|
|
||||||
|
writeTreeFile(t, repository, "README.md", []byte("worktree changed\n"))
|
||||||
|
statusBefore := gitTree(t, repository, "status", "--porcelain=v1", "--untracked-files=all")
|
||||||
|
tree, err := ReadCandidateTree(context.Background(), repository, commit)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadCandidateTree() error = %v", err)
|
||||||
|
}
|
||||||
|
entries := make(map[string]gitTreeEntry, len(tree.Entries()))
|
||||||
|
for _, entry := range tree.Entries() {
|
||||||
|
entries[entry.Path] = gitTreeEntry{mode: entry.Mode, kind: entry.Type, size: entry.Size, hasSize: entry.HasSize}
|
||||||
|
}
|
||||||
|
for _, path := range []string{"vendor/library/go.mod", "nested/module/go.mod", unusualPath} {
|
||||||
|
if _, ok := entries[path]; !ok {
|
||||||
|
t.Errorf("committed tree is missing %q", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got := entries["binary.dat"]; got.size != 4 || !got.hasSize {
|
||||||
|
t.Errorf("binary metadata = %#v, want size 4", got)
|
||||||
|
}
|
||||||
|
if got := entries["readme-link"]; got.mode != "120000" || got.kind != "blob" || !got.hasSize {
|
||||||
|
t.Errorf("symlink metadata = %#v", got)
|
||||||
|
}
|
||||||
|
if got := entries["submodule"]; got.mode != "160000" || got.kind != "commit" || got.hasSize {
|
||||||
|
t.Errorf("gitlink metadata = %#v", got)
|
||||||
|
}
|
||||||
|
contents, err := tree.ReadFile(context.Background(), "README.md")
|
||||||
|
if err != nil || string(contents) != "captured\n" {
|
||||||
|
t.Fatalf("captured README = %q, error = %v", contents, err)
|
||||||
|
}
|
||||||
|
binary, err := tree.ReadFile(context.Background(), "binary.dat")
|
||||||
|
if err != nil || !bytes.Equal(binary, []byte{0, 1, 255, 2}) {
|
||||||
|
t.Fatalf("captured binary = %v, error = %v", binary, err)
|
||||||
|
}
|
||||||
|
unusual, err := tree.ReadFile(context.Background(), unusualPath)
|
||||||
|
if err != nil || string(unusual) != "unusual path\n" {
|
||||||
|
t.Fatalf("unusual path contents = %q, error = %v", unusual, err)
|
||||||
|
}
|
||||||
|
if _, err := tree.ReadFile(context.Background(), "submodule"); err == nil {
|
||||||
|
t.Fatal("ReadFile() returned submodule commit contents")
|
||||||
|
}
|
||||||
|
statusAfter := gitTree(t, repository, "status", "--porcelain=v1", "--untracked-files=all")
|
||||||
|
if statusAfter != statusBefore {
|
||||||
|
t.Fatalf("tree inspection changed worktree state: before %q, after %q", statusBefore, statusAfter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadCandidateTreePreservesGitProcessError(t *testing.T) {
|
||||||
|
repository := newTreeRepository(t)
|
||||||
|
_, err := ReadCandidateTree(context.Background(), repository, strings.Repeat("f", 40))
|
||||||
|
var exitErr *exec.ExitError
|
||||||
|
if !errors.As(err, &exitErr) {
|
||||||
|
t.Fatalf("ReadCandidateTree() error = %v, want Git process error preserved", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type gitTreeEntry struct {
|
||||||
|
mode string
|
||||||
|
kind string
|
||||||
|
size int64
|
||||||
|
hasSize bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTreeRepository(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
repository := t.TempDir()
|
||||||
|
gitTree(t, repository, "init", "-b", "main")
|
||||||
|
gitTree(t, repository, "config", "user.name", "ReleaseKit Test")
|
||||||
|
gitTree(t, repository, "config", "user.email", "releasekit@example.invalid")
|
||||||
|
return repository
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeTreeFile(t *testing.T, repository, name string, content []byte) {
|
||||||
|
t.Helper()
|
||||||
|
path := filepath.Join(repository, filepath.FromSlash(name))
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, content, 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func gitTree(t *testing.T, repository string, args ...string) string {
|
||||||
|
t.Helper()
|
||||||
|
command := exec.Command("git", args...)
|
||||||
|
command.Dir = repository
|
||||||
|
output, err := command.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, output)
|
||||||
|
}
|
||||||
|
return string(output)
|
||||||
|
}
|
||||||
@@ -20,6 +20,12 @@ type Request struct {
|
|||||||
Settings Settings
|
Settings Settings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReadCandidateTree loads the committed-file inventory when a selected check
|
||||||
|
// needs file-oriented input.
|
||||||
|
func (request Request) ReadCandidateTree(ctx context.Context) (*CandidateTree, error) {
|
||||||
|
return ReadCandidateTree(ctx, request.RepositoryDir, request.Commit)
|
||||||
|
}
|
||||||
|
|
||||||
// Run executes shared validation for the captured candidate.
|
// Run executes shared validation for the captured candidate.
|
||||||
func Run(ctx context.Context, request Request) error {
|
func Run(ctx context.Context, request Request) error {
|
||||||
tool, err := gotool.New(ctx, request.RepositoryDir)
|
tool, err := gotool.New(ctx, request.RepositoryDir)
|
||||||
|
|||||||
Reference in New Issue
Block a user