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}