layout.go

  1package checkout
  2
  3import (
  4	"context"
  5	"fmt"
  6	"os"
  7	"path/filepath"
  8	"strings"
  9
 10	"git.kilimanjaro.io/ci/internal/lex"
 11)
 12
 13// Main says where the main checkout comes from.
 14type Main struct {
 15	// Repo is the repository name; the checkout is <run>/<Repo>.
 16	Repo string
 17	// WorkingCopy, if set, is linked as the main checkout (local runs
 18	// without --clean): the run sees uncommitted changes.
 19	WorkingCopy string
 20	// Otherwise the main checkout is a clean clone of Sha from From (a local
 21	// git dir, or a mirror) with Origin as its origin remote.
 22	From   string
 23	Sha    string
 24	Origin string
 25}
 26
 27// Checked is one prepared checkout.
 28type Checked struct {
 29	Repo string
 30	Path string // absolute
 31	Rev  string // commit, or "" for a linked working copy
 32	Env  string // variable that receives Path
 33	Link bool   // a symlink to a local directory (working copy or --repo)
 34}
 35
 36// Layout prepares <runDir>/<main.Repo> and the extra repos next to it.
 37// Overrides link an extra repo to a local directory instead of cloning it.
 38func Layout(ctx context.Context, src *Source, runDir string, main Main, extras []lex.Repo, overrides map[string]string) ([]Checked, error) {
 39	root := filepath.Join(runDir, main.Repo)
 40	var out []Checked
 41	if main.WorkingCopy != "" {
 42		if err := link(main.WorkingCopy, root); err != nil {
 43			return nil, err
 44		}
 45		out = append(out, Checked{Repo: main.Repo, Path: root, Link: true})
 46	} else {
 47		if err := Clone(ctx, main.From, main.Sha, root, main.Origin); err != nil {
 48			return nil, fmt.Errorf("checkout %s: %w", main.Repo, err)
 49		}
 50		out = append(out, Checked{Repo: main.Repo, Path: root, Rev: main.Sha})
 51	}
 52
 53	for name := range overrides {
 54		found := false
 55		for _, r := range extras {
 56			found = found || r.Repo == name
 57		}
 58		if !found {
 59			return nil, fmt.Errorf("--repo %s: the workflow has no repo %q", name, name)
 60		}
 61	}
 62	for _, r := range extras {
 63		dest := filepath.Clean(filepath.Join(root, r.Path))
 64		if !Inside(runDir, dest) || dest == runDir || Inside(root, dest) {
 65			return nil, fmt.Errorf("repo %s: path %q must be a sibling of the main checkout (like ../%s)", r.Repo, r.Path, r.Repo)
 66		}
 67		if p, ok := overrides[r.Repo]; ok {
 68			if err := link(p, dest); err != nil {
 69				return nil, err
 70			}
 71			out = append(out, Checked{Repo: r.Repo, Path: dest, Env: r.Env, Link: true})
 72			continue
 73		}
 74		mirror, err := src.Mirror(ctx, r.Repo)
 75		if err != nil {
 76			return nil, err
 77		}
 78		sha, err := Resolve(ctx, mirror, r.Rev)
 79		if err != nil {
 80			return nil, fmt.Errorf("repo %s: %w", r.Repo, err)
 81		}
 82		url, _ := src.URL(r.Repo)
 83		if err := Clone(ctx, mirror, sha, dest, url); err != nil {
 84			return nil, fmt.Errorf("checkout %s: %w", r.Repo, err)
 85		}
 86		out = append(out, Checked{Repo: r.Repo, Path: dest, Rev: sha, Env: r.Env})
 87	}
 88	return out, nil
 89}
 90
 91func link(target, at string) error {
 92	abs, err := filepath.Abs(target)
 93	if err != nil {
 94		return err
 95	}
 96	if fi, err := os.Stat(abs); err != nil || !fi.IsDir() {
 97		return fmt.Errorf("%s is not a directory", target)
 98	}
 99	if err := os.MkdirAll(filepath.Dir(at), 0o755); err != nil {
100		return err
101	}
102	return os.Symlink(abs, at)
103}
104
105// SiblingURL derives a repo's clone URL from another repo's URL on the same
106// forge by replacing the last path element: git@host:sovrn -> git@host:servers,
107// https://host/u/sovrn.git -> https://host/u/servers.git.
108func SiblingURL(of, repo string) (string, error) {
109	i := strings.LastIndexAny(of, "/:")
110	if i < 0 || i == len(of)-1 {
111		return "", fmt.Errorf("can't derive a clone URL for %s from %q", repo, of)
112	}
113	suffix := ""
114	if strings.HasSuffix(of, ".git") {
115		suffix = ".git"
116	}
117	return of[:i+1] + repo + suffix, nil
118}
119
120// RepoName returns the last path element of a clone URL without .git
121// (git@host:sovrn.git -> sovrn), or "".
122func RepoName(url string) string {
123	url = strings.TrimSuffix(strings.TrimSuffix(url, "/"), ".git")
124	i := strings.LastIndexAny(url, "/:")
125	if i < 0 || i == len(url)-1 {
126		return ""
127	}
128	return url[i+1:]
129}
130
131// URLFunc returns the clone-URL resolver: a template with {repo} (from
132// CI_CLONE_URL) if set, else a sibling of origin.
133func URLFunc(template, origin string) func(string) (string, error) {
134	return func(repo string) (string, error) {
135		if template != "" {
136			if !strings.Contains(template, "{repo}") {
137				return "", fmt.Errorf("CI_CLONE_URL %q has no {repo}", template)
138			}
139			return strings.ReplaceAll(template, "{repo}", repo), nil
140		}
141		if origin == "" {
142			return "", fmt.Errorf("can't find a clone URL for %s: the main repo has no origin remote; set CI_CLONE_URL (e.g. git@host:{repo})", repo)
143		}
144		return SiblingURL(origin, repo)
145	}
146}
147
148// OriginURL returns the origin remote URL of the repository at gitDir, or "".
149func OriginURL(ctx context.Context, gitDir string) string {
150	out, err := git(ctx, gitDir, "config", "--get", "remote.origin.url")
151	if err != nil {
152		return ""
153	}
154	return strings.TrimSpace(out)
155}