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