Skip to content
quarry Artifact [d8d46fa855]
Login

Artifact [d8d46fa855]

Artifact d8d46fa855dab89146545838ae27bea3d836d8b36bb7580eb3d4466dbe5cb913:


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
}