Read files from captured Git commits
This commit is contained in:
@@ -1,12 +1,26 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"strconv"
|
||||
"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 {
|
||||
Head string
|
||||
Status string
|
||||
@@ -74,3 +88,112 @@ func ReadCommitFile(ctx context.Context, repositoryDir, commit, name string) ([]
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user