Skip to content
quarry Artifact [1bc66708ff]
Login

Artifact [1bc66708ff]

Artifact 1bc66708ff485b39fa5c9aa4a4266c7f54db027c7c651e93b6ff8c780cbfbec5:


package zitcli

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os/exec"
	"strings"
)

// MergeReport is zit's account of merging one check-in into another,
// file by file and region by region (`zit merge-tree -F json`,
// danmestas/zit#231). It comes from the same loop that does the merge,
// so a conflict view built on it shows exactly what `zit merge` leaves
// in a checkout; quarry never merges or parses conflict markers itself.
//
// zit's names are its own: current is the check-in merged INTO (a
// landing's target), other is the one merged (the branch). The forge
// says ours and theirs, and translates in exactly one place.
type MergeReport struct {
	Verdict string      `json:"verdict"`
	Base    string      `json:"base"`
	Current string      `json:"current"`
	Other   string      `json:"other"`
	Files   []MergeFile `json:"files"`
}

// MergeFile is one file the merge could not settle on its own: its
// kind, each side's blob, and, for text, the regions in order.
type MergeFile struct {
	Path    string            `json:"path"`
	Kind    string            `json:"kind"`
	Base    string            `json:"base"`
	Current string            `json:"current"`
	Other   string            `json:"other"`
	Mode    map[string]string `json:"mode"`
	Regions []MergeRegion     `json:"regions"`
}

// MergeRegion is one stretch of a file, as half-open line ranges in
// each side's own lines: same, current (changed only on current's
// side, and merged), other (likewise), or conflict.
type MergeRegion struct {
	Kind    string `json:"kind"`
	Base    [2]int `json:"base"`
	Current [2]int `json:"current"`
	Other   [2]int `json:"other"`
}

// MergeRegions asks zit for the report of merging branchTip into
// targetTip, on the store, writing nothing. A clean merge answers a
// report with no files. Memoised on the pair: a check-in cannot change,
// so neither can its merge with another.
func (c *Client) MergeRegions(ctx context.Context, branchTip, targetTip string) (MergeReport, error) {
	key := "regions\x00" + branchTip + "\x00" + targetTip
	c.mergeMu.Lock()
	hit, ok := c.regionCache[key]
	c.mergeMu.Unlock()
	if ok {
		return hit, nil
	}
	cmd, done := c.command(ctx, "merge-tree", "-R", c.repo, "--into", targetTip, "-F", "json", "--", branchTip)
	defer done()
	var stdout, stderr bytes.Buffer
	cmd.Stdout, cmd.Stderr = &stdout, &stderr
	if err := cmd.Run(); err != nil {
		// Exit 5 is a conflict, and the report is the answer, not a
		// failure.
		var exit *exec.ExitError
		if !errors.As(err, &exit) || exit.ExitCode() != 5 {
			msg := strings.TrimSpace(stderr.String())
			if missingVersion(msg) {
				return MergeReport{}, fmt.Errorf("%w: %.12s into %.12s: %s", ErrNoVersion, branchTip, targetTip, msg)
			}
			return MergeReport{}, fmt.Errorf("zitcli: merge-tree -F json %.12s into %.12s: %w: %s", branchTip, targetTip, err, msg)
		}
	}
	var rep MergeReport
	if err := json.Unmarshal(stdout.Bytes(), &rep); err != nil {
		return MergeReport{}, fmt.Errorf("zitcli: merge-tree -F json %.12s into %.12s: %w", branchTip, targetTip, err)
	}
	c.mergeMu.Lock()
	if c.regionCache == nil || len(c.regionCache) >= regionCacheLimit {
		c.regionCache = map[string]MergeReport{}
	}
	c.regionCache[key] = rep
	c.mergeMu.Unlock()
	return rep, nil
}

// regionCacheLimit bounds the memo: a report carries every conflicted
// file's regions, and open landings are few.
const regionCacheLimit = 64