Skip to content
quarry Artifact [6b5791e6c8]
Login

Artifact [6b5791e6c8]

Artifact 6b5791e6c8087fde7c8098859b90ead66dbf25f3ee73868746ff9d57a41bfe80:


package zitcli

import (
	"bufio"
	"fmt"
	"io"
	"os/exec"
	"strconv"
	"strings"

	"craftdesign.group/quarry/internal/artifact"
)

// CrosslinkRecord is one check-in's derived record from `zit crosslink`
// (zit#109): the answers an indexer otherwise re-derives by parsing and
// expanding every manifest itself.
type CrosslinkRecord struct {
	Hash        string
	MtimeMillis int64
	Parents     []string
	User        string
	Comment     string
	Tags        []artifact.Tag
	// Changes holds every path the check-in touched (adds, edits and
	// deletes alike — quarry's filechange table records "touched").
	// FilesKnown false mirrors the stream's "files unknown": the verb
	// never claims what it did not compute, and neither do we.
	Changes    []string
	FilesKnown bool
}

// Crosslink streams every check-in's derived record in persisted order.
func (c *Client) Crosslink(fn func(CrosslinkRecord) error) error {
	cmd := exec.Command("zit", "crosslink", "-R", c.repo)
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return err
	}
	if err := cmd.Start(); err != nil {
		return err
	}
	perr := parseCrosslink(bufio.NewReaderSize(stdout, 1<<20), fn)
	if perr != nil {
		cmd.Process.Kill()
	}
	werr := cmd.Wait()
	if perr != nil {
		return fmt.Errorf("zitcli: crosslink: %w", perr)
	}
	if werr != nil {
		return fmt.Errorf("zitcli: crosslink: %w", werr)
	}
	return nil
}

func parseCrosslink(r *bufio.Reader, fn func(CrosslinkRecord) error) error {
	header, err := r.ReadString('\n')
	if err != nil {
		return fmt.Errorf("reading header: %w", err)
	}
	if strings.TrimSpace(header) != "crosslink 1" {
		return fmt.Errorf("unknown stream format %q", strings.TrimSpace(header))
	}
	var rec CrosslinkRecord
	open := false
	for {
		line, err := r.ReadString('\n')
		if err == io.EOF && line == "" {
			if open {
				return fmt.Errorf("stream ends inside record %s", rec.Hash)
			}
			return nil
		}
		if err != nil {
			return err
		}
		line = strings.TrimSuffix(line, "\n")
		fields := strings.Fields(line)
		if len(fields) == 0 {
			continue
		}
		switch fields[0] {
		case "checkin":
			if len(fields) < 3 {
				return fmt.Errorf("short checkin line %q", line)
			}
			ms, perr := strconv.ParseInt(fields[2], 10, 64)
			if perr != nil {
				return fmt.Errorf("bad mtime in %q", line)
			}
			// The artifact parser's §4.7.17 default applies here too:
			// a manifest with no U card is anonymous.
			rec = CrosslinkRecord{Hash: fields[1], MtimeMillis: ms,
				Parents: fields[3:], User: "anonymous", FilesKnown: true}
			open = true
		case "user":
			if len(fields) == 2 {
				rec.User = artifact.DecodeToken(fields[1])
			}
		case "comment":
			if len(fields) != 2 {
				return fmt.Errorf("bad comment line %q", line)
			}
			size, perr := strconv.Atoi(fields[1])
			if perr != nil || size < 0 {
				return fmt.Errorf("bad comment length %q", line)
			}
			raw := make([]byte, size)
			if _, rerr := io.ReadFull(r, raw); rerr != nil {
				return fmt.Errorf("comment body of %s: %w", rec.Hash, rerr)
			}
			if nl, rerr := r.ReadByte(); rerr != nil || nl != '\n' {
				return fmt.Errorf("comment of %s: missing terminator", rec.Hash)
			}
			rec.Comment = string(raw)
		case "tag":
			if len(fields) < 3 {
				return fmt.Errorf("short tag line %q", line)
			}
			t := artifact.Tag{Name: artifact.DecodeToken(fields[1]), Target: fields[2]}
			if len(fields) > 3 {
				t.Value = artifact.DecodeToken(fields[3])
			}
			rec.Tags = append(rec.Tags, t)
		case "file":
			if len(fields) != 3 {
				return fmt.Errorf("bad file line %q", line)
			}
			rec.Changes = append(rec.Changes, artifact.DecodeToken(fields[2]))
		case "files":
			if len(fields) == 2 && fields[1] == "unknown" {
				rec.FilesKnown = false
			}
		case "end":
			if !open {
				return fmt.Errorf("end outside a record")
			}
			if err := fn(rec); err != nil {
				return err
			}
			open = false
		default:
			// Unknown sections from a newer stream are skipped — the
			// versioned header is where incompatibility is declared.
		}
	}
}