package zitcli
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"regexp"
"strconv"
"strings"
"craftdesign.group/quarry/internal/lease"
)
// GitImport is what one `zit import-git` run reported: how many
// check-ins it added, and one line per upstream head that moved away
// from its old tip or vanished (a force-push upstream; the check-ins it
// left behind stay).
type GitImport struct {
Added int
Moved []string
}
// ImportedHead is the pinned source default's head from the last successful
// Git import. It is not Fossil's date-based trunk resolution: an abandoned
// leaf can be newer than the head the upstream now publishes (zit#252).
func (c *Client) ImportedHead(ctx context.Context) (name, hash string, err error) {
cmd, done := c.command(ctx, "info", "--imported-head", "-F", "json", "-R", c.repo)
defer done()
out, err := cmd.Output()
if err != nil {
return "", "", fmt.Errorf("zitcli: imported head: %w", err)
}
var head struct {
Name string `json:"name"`
Hash string `json:"hash"`
}
if err := json.Unmarshal(out, &head); err != nil {
return "", "", fmt.Errorf("zitcli: imported head: %w", err)
}
if !strings.HasPrefix(head.Name, "heads/") || len(head.Name) == len("heads/") || !immutableVersion(head.Hash) {
return "", "", fmt.Errorf("zitcli: imported head: invalid name or hash")
}
return head.Name, head.Hash, nil
}
var importAdded = regexp.MustCompile(`added (\d+) check-in`)
func parseGitImport(out string) GitImport {
var r GitImport
for _, l := range strings.Split(out, "\n") {
l = strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(l), "import-git:"))
if m := importAdded.FindStringSubmatch(l); m != nil {
r.Added, _ = strconv.Atoi(m[1])
continue
}
if strings.HasPrefix(l, "head ") {
r.Moved = append(r.Moved, l)
}
}
return r
}
// ImportGit makes the store at dest from the Git work tree at source:
// the first import of a repository that follows a Git upstream (#358).
// Flat manifests, so plain fossil can clone the result as it can any
// store quarry makes. dest must not exist; zit writes it only when the
// import completes. No lease: the store is not served yet.
func ImportGit(ctx context.Context, source, dest string, env []string) (GitImport, error) {
return runImportGit(ctx, "", source, dest, env, false)
}
// ImportGitInto adds the upstream commits the store does not yet hold
// (zit import-git --into). A write under the store's lease; on any
// failure zit leaves the store as it was.
func (c *Client) ImportGitInto(tok lease.Token, ctx context.Context, source string, env []string) (GitImport, error) {
if err := tok.Err(); err != nil {
return GitImport{}, err
}
ctx, stop := underLease(ctx, tok)
defer stop()
return runImportGit(ctx, repoName(c.repo), source, c.repo, env, true)
}
func runImportGit(ctx context.Context, repo, source, dest string, env []string, into bool) (GitImport, error) {
args := []string{"import-git", source, dest, "--no-submanifests"}
if into {
args = append(args, "--into")
}
if err := checkArgs(args); err != nil {
return GitImport{}, err
}
if repo == "" {
repo = repoName(dest)
}
cmd, done := spawnUnattended(ctx, repo, "git-import", args...)
defer done()
if cmd.Err != nil {
return GitImport{}, cmd.Err
}
// No inherited GIT_* variable reaches the git zit runs: one such as
// GIT_ALLOW_PROTOCOL would override what env sets.
cmd.Env = append(withoutGitEnv(os.Environ()), env...)
OwnGroup(cmd)
cmd.WaitDelay = waitDelay
var out bytes.Buffer
cmd.Stdout, cmd.Stderr = &out, &out
err := cmd.Run()
text := strings.TrimSpace(out.String())
if err != nil {
return GitImport{}, fmt.Errorf("zit import-git: %s", lastLine(text, err))
}
return parseGitImport(text), nil
}
// lastLine is the import's last line of output, which is its refusal
// when it refuses, else the process error.
func lastLine(text string, err error) string {
if i := strings.LastIndexByte(text, '\n'); i >= 0 {
text = text[i+1:]
}
text = strings.TrimSpace(strings.TrimPrefix(text, "import-git:"))
if text == "" {
return err.Error()
}
return text
}
func withoutGitEnv(env []string) []string {
out := env[:0:0]
for _, kv := range env {
if !strings.HasPrefix(kv, "GIT_") {
out = append(out, kv)
}
}
return out
}