Skip to content
quarry Artifact [2af1258411]
Login

Artifact [2af1258411]

Artifact 2af1258411eb2258e34a1030d98414de6e627776e52b12afded98a91defd81a8:


package zitcli

import (
	"archive/tar"
	"bytes"
	"compress/gzip"
	"context"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"
)

// MergeConflict is a pair that does not merge: the answer a merged tree
// has instead of a tree. State is zit's word for it ("conflict" or
// "unrelated") and Paths the files both sides changed irreconcilably.
type MergeConflict struct {
	State string
	Paths []string
}

func (e *MergeConflict) Error() string {
	if len(e.Paths) == 0 {
		return "the merge does not apply: " + e.State
	}
	return "the merge does not apply: " + e.State + " in " + strings.Join(e.Paths, ", ")
}

// MergeArchiveTo writes, as a gzip tarball at dst, the tree an accept
// of branchTip onto targetTip WOULD produce, with nothing written to the
// store: `zit merge-tree` (danmestas/zit#223) merges on the store alone
// and writes plain files, the same merge `merge --probe` and `landing
// accept` judge. A pair that does not merge answers a *MergeConflict and
// writes no file.
//
// The tree is packed here, under a top-level <name>/ directory, which is
// the shape the forge's check-in archives have and the runner unpacks.
func (c *Client) MergeArchiveTo(ctx context.Context, branchTip, targetTip, name, dst string, max int64) error {
	dir, err := os.MkdirTemp("", "quarry-merge-")
	if err != nil {
		return err
	}
	defer os.RemoveAll(dir)
	tree := filepath.Join(dir, "tree")
	cmd, done := c.command(ctx, "merge-tree", "-R", c.repo, "--into", targetTip, "--out", tree, "--", branchTip)
	defer done()
	var stdout, stderr bytes.Buffer
	cmd.Stdout, cmd.Stderr = &stdout, &stderr
	if err := cmd.Run(); err != nil {
		msg := strings.TrimSpace(stderr.String())
		var exit *exec.ExitError
		switch {
		case errors.As(err, &exit) && exit.ExitCode() == 5:
			return &MergeConflict{State: "conflict", Paths: strings.Fields(stdout.String())}
		case strings.Contains(msg, "unrelated") || strings.Contains(msg, "no common ancestor"):
			return &MergeConflict{State: "unrelated"}
		case missingVersion(msg):
			return fmt.Errorf("%w: %.12s into %.12s: %s", ErrNoVersion, branchTip, targetTip, msg)
		}
		return fmt.Errorf("zitcli: merge-tree %.12s into %.12s: %w: %s", branchTip, targetTip, err, msg)
	}
	return tarTree(tree, name, dst, max)
}

// tarTree packs root as <name>/... into a gzip tarball at dst.
// Timestamps are fixed so the same tree packs to the same bytes.
func tarTree(root, name, dst string, max int64) error {
	f, err := os.Create(dst)
	if err != nil {
		return err
	}
	gz := gzip.NewWriter(f)
	tw := tar.NewWriter(gz)
	epoch := time.Unix(0, 0).UTC()
	var total int64
	werr := filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		rel, err := filepath.Rel(root, p)
		if err != nil || rel == "." {
			return err
		}
		info, err := d.Info()
		if err != nil {
			return err
		}
		h := &tar.Header{Name: name + "/" + filepath.ToSlash(rel), ModTime: epoch, Format: tar.FormatPAX}
		switch {
		case d.IsDir():
			h.Typeflag, h.Name, h.Mode = tar.TypeDir, h.Name+"/", 0o755
			return tw.WriteHeader(h)
		case info.Mode()&fs.ModeSymlink != 0:
			link, err := os.Readlink(p)
			if err != nil {
				return err
			}
			h.Typeflag, h.Linkname, h.Mode = tar.TypeSymlink, link, 0o777
			return tw.WriteHeader(h)
		case info.Mode().IsRegular():
			total += info.Size()
			if total > max {
				return fmt.Errorf("zitcli: merged tree exceeds the %d-byte limit", max)
			}
			h.Typeflag, h.Size, h.Mode = tar.TypeReg, info.Size(), 0o644
			if info.Mode().Perm()&0o111 != 0 {
				h.Mode = 0o755
			}
			if err := tw.WriteHeader(h); err != nil {
				return err
			}
			src, err := os.Open(p)
			if err != nil {
				return err
			}
			_, err = io.Copy(tw, src)
			src.Close()
			return err
		}
		return nil
	})
	for _, closer := range []io.Closer{tw, gz, f} {
		if err := closer.Close(); err != nil && werr == nil {
			werr = err
		}
	}
	if werr != nil {
		os.Remove(dst)
	}
	return werr
}