@sous-io/sous 0.2.27 → 0.2.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/markdown/_sidebar.md +1 -0
- package/docs/markdown/repositories-authoring.md +12 -8
- package/docs/markdown/repositories-consuming.md +8 -1
- package/docs/markdown/repositories-file-formats.md +53 -8
- package/docs/markdown/repositories-providers.md +8 -6
- package/docs/markdown/repositories-troubleshooting.md +6 -0
- package/docs/markdown/repositories-variables.md +9 -2
- package/docs/markdown/repositories.md +3 -3
- package/package.json +1 -1
- package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +11 -7
- package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
- package/src/commands/lock/rebuild.ts +2 -2
- package/src/commands/repo/contribute.ts +1 -10
- package/src/commands/repo/gc.ts +3 -2
- package/src/commands/repo/release.ts +20 -3
- package/src/commands/repo/search.ts +2 -1
- package/src/commands/vars/ask.ts +32 -10
- package/src/lib/refs/find.ts +202 -21
- package/src/lib/refs/index.ts +2 -0
- package/src/lib/refs/parse.ts +844 -0
- package/src/lib/refs/scopes.ts +34 -0
- package/src/lib/refs/settle.ts +219 -0
- package/src/lib/repos/catalog.ts +44 -25
- package/src/lib/repos/formats/index-file.ts +2 -1
- package/src/lib/repos/formats/recipe-manifest.ts +10 -8
- package/src/lib/repos/index.ts +0 -1
- package/src/lib/repos/lock-service.ts +3 -2
- package/src/lib/repos/locked-namespace-resolver.ts +21 -15
- package/src/lib/repos/locked-recipes.ts +2 -2
- package/src/lib/repos/namespace-resolver.ts +40 -47
- package/src/lib/repos/providers/base.ts +30 -0
- package/src/lib/repos/providers/github.ts +22 -0
- package/src/lib/repos/providers/gitlab.ts +68 -0
- package/src/lib/repos/providers/provider.ts +96 -0
- package/src/lib/repos/release/index-builder.ts +55 -17
- package/src/lib/repos/release/index.ts +1 -0
- package/src/lib/repos/release/plan.ts +8 -6
- package/src/lib/repos/release/settle.ts +237 -0
- package/src/lib/repos/release/tags.ts +3 -7
- package/src/lib/repos/resolver.ts +198 -41
- package/src/lib/repos/subscription-service.ts +262 -38
- package/src/lib/vars/mappings.ts +17 -30
- package/src/lib/repos/ref.ts +0 -513
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settling, at release time, the dependencies that read more than one way.
|
|
3
|
+
*
|
|
4
|
+
* Most dependencies say exactly what they mean: `workflow/alpha`, or
|
|
5
|
+
* `github://owner/repo/workflow/alpha`. Two kinds do not:
|
|
6
|
+
*
|
|
7
|
+
* - a GitLab URL with nested groups, which does not say where the project path
|
|
8
|
+
* ends (`gitlab.com/a/b/c/d` may be project `a/b` with namespace `c` and
|
|
9
|
+
* recipe `d`, or project `a/b/c` with namespace `d`);
|
|
10
|
+
* - a browser URL copied from a host's file view, which names a folder whose
|
|
11
|
+
* recipe only that repository's index can say.
|
|
12
|
+
*
|
|
13
|
+
* `sous repo release` settles each of them once, by fetching the index of every
|
|
14
|
+
* candidate repository through the provider layer and keeping the reading whose
|
|
15
|
+
* index publishes what was named. What it settled on is recorded in the index
|
|
16
|
+
* beside the dependency's key, so a consumer reads the answer and never probes.
|
|
17
|
+
*
|
|
18
|
+
* A network failure fails the release: a release never falls through to the
|
|
19
|
+
* next reading, because the reading it fell through to could be the wrong one.
|
|
20
|
+
* Two readings that both publish what is named are a genuine tie, and an error
|
|
21
|
+
* naming the spellings that read one way (the `/*` form for the namespace).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { parseIndexFile, type IndexFile } from "../formats/index-file.js";
|
|
25
|
+
import { INDEX_FILENAME } from "../formats/common.js";
|
|
26
|
+
import {
|
|
27
|
+
builtInProviders,
|
|
28
|
+
providerById,
|
|
29
|
+
type ProviderOptions,
|
|
30
|
+
type RepoProvider,
|
|
31
|
+
} from "../providers/index.js";
|
|
32
|
+
import {
|
|
33
|
+
formatRef,
|
|
34
|
+
isBrowsedReading,
|
|
35
|
+
isNamedReading,
|
|
36
|
+
parseRef,
|
|
37
|
+
refKey,
|
|
38
|
+
type ParsedRef,
|
|
39
|
+
type RefLocation,
|
|
40
|
+
type RefReading,
|
|
41
|
+
} from "../../refs/parse.js";
|
|
42
|
+
import { settleInIndex } from "../../refs/settle.js";
|
|
43
|
+
import { RefSource } from "../../refs/scopes.js";
|
|
44
|
+
import { describeError, type RepoValidation, type ValidationProblem } from "./validate.js";
|
|
45
|
+
|
|
46
|
+
/** What one dependency settled on. */
|
|
47
|
+
export type SettledDependency = {
|
|
48
|
+
/** The canonical identity of the repository publishing it. */
|
|
49
|
+
identity: string;
|
|
50
|
+
/**
|
|
51
|
+
* The recipe keys it reaches in that repository: one for a recipe, and every
|
|
52
|
+
* recipe the namespace publishes for a namespace.
|
|
53
|
+
*/
|
|
54
|
+
keys: string[];
|
|
55
|
+
/** The range the manifest wrote, when it wrote one. */
|
|
56
|
+
range?: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** What settling found. */
|
|
60
|
+
export type SettleResult = {
|
|
61
|
+
/** Each settled dependency, keyed by the dependency exactly as written (trimmed). */
|
|
62
|
+
settled: Map<string, SettledDependency>;
|
|
63
|
+
/** Everything that could not be settled; every one of them is an error. */
|
|
64
|
+
problems: ValidationProblem[];
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** How settling reaches the network. */
|
|
68
|
+
export type SettleOptions = {
|
|
69
|
+
/** The providers to fetch through. Defaults to the built-ins. */
|
|
70
|
+
providers?: RepoProvider[];
|
|
71
|
+
/** Testing seams handed to every index fetch. */
|
|
72
|
+
providerOptions?: ProviderOptions;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* True when a dependency's readings need an index to settle: it reads more than
|
|
77
|
+
* one way, or it names a browser path.
|
|
78
|
+
*
|
|
79
|
+
* @param readings - Every reading of the dependency.
|
|
80
|
+
*/
|
|
81
|
+
export function needsSettling(readings: RefReading[]): boolean {
|
|
82
|
+
return readings.length > 1 || readings.some(isBrowsedReading);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Settles every dependency in a repository that reads more than one way.
|
|
87
|
+
* Dependencies that read one way are left alone, and nothing is fetched for
|
|
88
|
+
* them, so a repository that uses none of these forms releases offline as
|
|
89
|
+
* before.
|
|
90
|
+
*
|
|
91
|
+
* @param validation - The validated repository.
|
|
92
|
+
* @param options - The providers, and testing seams.
|
|
93
|
+
*/
|
|
94
|
+
export async function settleDependencyLocations(
|
|
95
|
+
validation: RepoValidation,
|
|
96
|
+
options: SettleOptions = {}
|
|
97
|
+
): Promise<SettleResult> {
|
|
98
|
+
const providers = options.providers ?? builtInProviders();
|
|
99
|
+
const settled = new Map<string, SettledDependency>();
|
|
100
|
+
const problems: ValidationProblem[] = [];
|
|
101
|
+
const indexes = new Map<string, Promise<IndexFile>>();
|
|
102
|
+
|
|
103
|
+
const indexOf = (location: RefLocation): Promise<IndexFile> => {
|
|
104
|
+
let pending = indexes.get(location.identity);
|
|
105
|
+
if (pending === undefined) {
|
|
106
|
+
pending = fetchIndexAt(location, providers, options.providerOptions ?? {});
|
|
107
|
+
indexes.set(location.identity, pending);
|
|
108
|
+
}
|
|
109
|
+
return pending;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
for (const recipe of validation.recipes) {
|
|
113
|
+
const declared = [...(recipe.manifest.depends ?? []), ...(recipe.manifest.subscribes ?? [])];
|
|
114
|
+
for (const written of declared) {
|
|
115
|
+
const trimmed = written.trim();
|
|
116
|
+
if (settled.has(trimmed)) continue;
|
|
117
|
+
|
|
118
|
+
let readings: RefReading[];
|
|
119
|
+
try {
|
|
120
|
+
readings = parseRef(trimmed, RefSource.Manifest, providers);
|
|
121
|
+
} catch {
|
|
122
|
+
// Validation already reported a dependency that does not parse.
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (!needsSettling(readings)) continue;
|
|
126
|
+
|
|
127
|
+
const where = `${recipe.path} ('${trimmed}')`;
|
|
128
|
+
try {
|
|
129
|
+
settled.set(trimmed, await settleOne(trimmed, readings, indexOf, providers));
|
|
130
|
+
} catch (error) {
|
|
131
|
+
problems.push({ level: "error", where, message: describeError(error) });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { settled, problems };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Settles one dependency: fetches the index behind each reading, and keeps the
|
|
141
|
+
* one reading that publishes what it names.
|
|
142
|
+
*
|
|
143
|
+
* @param written - The dependency as written.
|
|
144
|
+
* @param readings - Every reading of it.
|
|
145
|
+
* @param indexOf - Fetches (once) the index at a location.
|
|
146
|
+
* @param providers - The providers, for formatting.
|
|
147
|
+
*/
|
|
148
|
+
async function settleOne(
|
|
149
|
+
written: string,
|
|
150
|
+
readings: RefReading[],
|
|
151
|
+
indexOf: (location: RefLocation) => Promise<IndexFile>,
|
|
152
|
+
providers: RepoProvider[]
|
|
153
|
+
): Promise<SettledDependency> {
|
|
154
|
+
const found: Array<{ reading: RefReading; settled: ParsedRef[]; index: IndexFile }> = [];
|
|
155
|
+
|
|
156
|
+
for (const reading of readings) {
|
|
157
|
+
const location = reading.location;
|
|
158
|
+
/* c8 ignore next */
|
|
159
|
+
if (location === undefined) continue;
|
|
160
|
+
|
|
161
|
+
let index: IndexFile;
|
|
162
|
+
try {
|
|
163
|
+
index = await indexOf(location);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`the dependency reads ${readings.length === 1 ? "as a folder in" : "as something in"} ` +
|
|
167
|
+
`the repository at ${location.url}, and its index could not be read, so the release ` +
|
|
168
|
+
`cannot settle what it means. A release never guesses past an index it could not ` +
|
|
169
|
+
`read.\n ${describeError(error)}`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const settled = settleInIndex(reading, {
|
|
174
|
+
namespaces: Object.keys(index.namespaces),
|
|
175
|
+
recipes: index.recipes,
|
|
176
|
+
});
|
|
177
|
+
if (settled.length > 0) found.push({ reading, settled, index });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (found.length === 0) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`no repository it could name publishes what it names. It was read as:\n` +
|
|
183
|
+
readings.map((reading) => ` ${formatRef(reading, providers)}`).join("\n")
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const choices = found.flatMap((entry) => entry.settled);
|
|
188
|
+
if (choices.length > 1) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`it names more than one thing, and each of these publishes it:\n` +
|
|
191
|
+
choices.map((choice) => ` ${formatRef(choice, providers)}`).join("\n") +
|
|
192
|
+
`\n Write the one you mean. A namespace is written with '/*' after it, as in ` +
|
|
193
|
+
`'${spelledOut(choices.find((choice) => choice.recipe === undefined) ?? choices[0]!, providers)}', ` +
|
|
194
|
+
`and a recipe as its canonical locator above.`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const { index } = found[0]!;
|
|
199
|
+
const choice = choices[0]!;
|
|
200
|
+
const keys =
|
|
201
|
+
choice.recipe === undefined
|
|
202
|
+
? Object.keys(index.recipes)
|
|
203
|
+
.filter((key) => key.startsWith(`${choice.namespace}/`))
|
|
204
|
+
.sort()
|
|
205
|
+
: [refKey(choice)];
|
|
206
|
+
const range = "range" in found[0]!.reading ? found[0]!.reading.range : undefined;
|
|
207
|
+
return {
|
|
208
|
+
identity: choice.location!.identity,
|
|
209
|
+
keys,
|
|
210
|
+
...(range === undefined ? {} : { range }),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** A reading spelled so it reads one way: a namespace with `/*` after it. */
|
|
215
|
+
function spelledOut(reading: ParsedRef, providers: RepoProvider[]): string {
|
|
216
|
+
const written = formatRef(reading, providers);
|
|
217
|
+
return reading.recipe === undefined && isNamedReading(reading) ? `${written}/*` : written;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Fetches and validates the index at a location, through its provider.
|
|
222
|
+
*
|
|
223
|
+
* @param location - Where the repository lives.
|
|
224
|
+
* @param providers - The providers to fetch through.
|
|
225
|
+
* @param options - Testing seams.
|
|
226
|
+
*/
|
|
227
|
+
async function fetchIndexAt(
|
|
228
|
+
location: RefLocation,
|
|
229
|
+
providers: RepoProvider[],
|
|
230
|
+
options: ProviderOptions
|
|
231
|
+
): Promise<IndexFile> {
|
|
232
|
+
const provider = providerById(location.provider, providers);
|
|
233
|
+
/* c8 ignore next */
|
|
234
|
+
if (provider === undefined) throw new Error(`sous has no '${location.provider}' provider.`);
|
|
235
|
+
const fetched = await provider.fetchIndex(provider.canonicalize(location.url), options);
|
|
236
|
+
return parseIndexFile(JSON.parse(fetched.text), `${location.url}/${INDEX_FILENAME}`);
|
|
237
|
+
}
|
|
@@ -16,6 +16,7 @@ import os from "node:os";
|
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
import { runGit, type RunOptions } from "../providers/git.js";
|
|
18
18
|
import { RECIPE_KEY_PATTERN } from "../formats/patterns.js";
|
|
19
|
+
import { splitRecipeKey } from "../../refs/parse.js";
|
|
19
20
|
|
|
20
21
|
/** One release tag, taken apart into the recipe it publishes. */
|
|
21
22
|
export type RecipeTag = {
|
|
@@ -60,13 +61,8 @@ export function parseRecipeTag(tag: string): RecipeTag | undefined {
|
|
|
60
61
|
const version = tag.slice(at + 1);
|
|
61
62
|
if (!RECIPE_KEY_PATTERN.test(key)) return undefined;
|
|
62
63
|
|
|
63
|
-
const
|
|
64
|
-
return {
|
|
65
|
-
tag,
|
|
66
|
-
namespace: key.slice(0, slash),
|
|
67
|
-
name: key.slice(slash + 1),
|
|
68
|
-
version,
|
|
69
|
-
};
|
|
64
|
+
const { namespace, name } = splitRecipeKey(key);
|
|
65
|
+
return { tag, namespace, name, version };
|
|
70
66
|
}
|
|
71
67
|
|
|
72
68
|
/**
|
|
@@ -27,14 +27,17 @@ import type { IndexFile } from "./formats/index-file.js";
|
|
|
27
27
|
import type { LockKind } from "./formats/lockfile.js";
|
|
28
28
|
import type { RecipeManifest } from "./formats/recipe-manifest.js";
|
|
29
29
|
import {
|
|
30
|
-
dependencyRefKey,
|
|
31
|
-
dependencyRepoUrl,
|
|
32
30
|
formatRef,
|
|
33
|
-
|
|
31
|
+
isBrowsedReading,
|
|
32
|
+
isNamedReading,
|
|
33
|
+
parseRef,
|
|
34
34
|
refKey,
|
|
35
|
-
|
|
35
|
+
splitRecipeKey,
|
|
36
36
|
type ParsedRef,
|
|
37
|
-
|
|
37
|
+
type RefReading,
|
|
38
|
+
} from "../refs/parse.js";
|
|
39
|
+
import { settleBrowsed, settleDependency, settleInIndex } from "../refs/settle.js";
|
|
40
|
+
import { RefSource } from "../refs/scopes.js";
|
|
38
41
|
import { shortNameFromIdentity } from "./identity.js";
|
|
39
42
|
import type { IndexDependency } from "./formats/index-file.js";
|
|
40
43
|
|
|
@@ -186,6 +189,15 @@ type WorkItem = RefRequest & {
|
|
|
186
189
|
provider?: string;
|
|
187
190
|
/** The dependency exactly as the manifest wrote it, for messages. */
|
|
188
191
|
written: string;
|
|
192
|
+
/**
|
|
193
|
+
* A browser path the dependency named, settled through the repository's
|
|
194
|
+
* index once the project has it. Until then `ref` holds no namespace.
|
|
195
|
+
*/
|
|
196
|
+
browsed?: string;
|
|
197
|
+
/** The range the manifest wrote, applied once a browsed path is settled. */
|
|
198
|
+
range?: string;
|
|
199
|
+
/** What the declaring version's index records, for pins once a browsed path is settled. */
|
|
200
|
+
pins?: Record<string, IndexDependency>;
|
|
189
201
|
};
|
|
190
202
|
};
|
|
191
203
|
|
|
@@ -200,24 +212,27 @@ type WorkItem = RefRequest & {
|
|
|
200
212
|
* version is what gets asked for, rather than whatever the declared range would
|
|
201
213
|
* reach today.
|
|
202
214
|
*
|
|
215
|
+
* A dependency that reads more than one way (a GitLab URL with nested groups)
|
|
216
|
+
* is settled by what the release recorded in the index, and failing that by
|
|
217
|
+
* what the project's own cached indexes already publish; nothing is fetched to
|
|
218
|
+
* find out. One that still reads several ways is an error naming them.
|
|
219
|
+
*
|
|
203
220
|
* @param written - The dependency as the manifest wrote it.
|
|
204
221
|
* @param parent - The recipe whose manifest declared it.
|
|
205
222
|
* @param kind - Whether it was declared as a dependency or a co-subscription.
|
|
223
|
+
* @param context - The added repositories and their cached indexes.
|
|
206
224
|
*/
|
|
207
225
|
function dependencyRequest(
|
|
208
226
|
written: string,
|
|
209
227
|
parent: ResolvedRecipe,
|
|
210
|
-
kind: LockKind
|
|
228
|
+
kind: LockKind,
|
|
229
|
+
context: ResolveContext
|
|
211
230
|
): WorkItem {
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (
|
|
217
|
-
ref.recipe = parsed.recipe;
|
|
218
|
-
const range = pinned?.version ?? parsed.range ?? pinned?.range;
|
|
219
|
-
if (range !== undefined) ref.range = range;
|
|
220
|
-
}
|
|
231
|
+
const reading = settleDependency(written, {
|
|
232
|
+
...(parent.dependencies === undefined ? {} : { recorded: parent.dependencies }),
|
|
233
|
+
known: (candidate) => knownLocally(candidate, context),
|
|
234
|
+
});
|
|
235
|
+
if (reading === undefined) throw unsettledDependencyError(written, parent);
|
|
221
236
|
|
|
222
237
|
const base = {
|
|
223
238
|
requestedBy: parent.key,
|
|
@@ -225,22 +240,143 @@ function dependencyRequest(
|
|
|
225
240
|
...(parent.prerelease ? { prerelease: true } : {}),
|
|
226
241
|
};
|
|
227
242
|
|
|
228
|
-
if (
|
|
243
|
+
if (isBrowsedReading(reading)) {
|
|
244
|
+
return {
|
|
245
|
+
ref: { namespace: "" },
|
|
246
|
+
...base,
|
|
247
|
+
remote: {
|
|
248
|
+
...remoteOf(reading, written),
|
|
249
|
+
browsed: reading.browsed,
|
|
250
|
+
...(reading.range === undefined ? {} : { range: reading.range }),
|
|
251
|
+
...(parent.dependencies === undefined ? {} : { pins: parent.dependencies }),
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/* c8 ignore next */
|
|
256
|
+
if (!isNamedReading(reading)) throw unsettledDependencyError(written, parent);
|
|
257
|
+
|
|
258
|
+
const pinned = parent.dependencies?.[refKey(reading)];
|
|
259
|
+
const ref = pinnedRef(reading, pinned);
|
|
260
|
+
|
|
261
|
+
if (reading.location === undefined) {
|
|
229
262
|
return { ref: { ...ref, repo: parent.repo }, ...base };
|
|
230
263
|
}
|
|
231
264
|
|
|
265
|
+
const remote = remoteOf(reading, written);
|
|
232
266
|
return {
|
|
233
267
|
ref,
|
|
234
268
|
...base,
|
|
235
|
-
remote: {
|
|
236
|
-
identity: pinned?.repo ?? parsed.canonicalRepo!,
|
|
237
|
-
url: dependencyRepoUrl(parsed)!,
|
|
238
|
-
...(parsed.provider === undefined ? {} : { provider: parsed.provider }),
|
|
239
|
-
written: written.trim(),
|
|
240
|
-
},
|
|
269
|
+
remote: { ...remote, identity: pinned?.repo ?? remote.identity },
|
|
241
270
|
};
|
|
242
271
|
}
|
|
243
272
|
|
|
273
|
+
/**
|
|
274
|
+
* A named reading as a request asks for it: at the exact version the index
|
|
275
|
+
* recorded when it recorded one, otherwise at the range the manifest wrote.
|
|
276
|
+
*
|
|
277
|
+
* @param reading - The reading.
|
|
278
|
+
* @param pinned - What the declaring version's index recorded for it.
|
|
279
|
+
*/
|
|
280
|
+
function pinnedRef(reading: ParsedRef, pinned: IndexDependency | undefined): ParsedRef {
|
|
281
|
+
const ref: ParsedRef = { namespace: reading.namespace };
|
|
282
|
+
if (reading.recipe !== undefined) {
|
|
283
|
+
ref.recipe = reading.recipe;
|
|
284
|
+
const range = pinned?.version ?? reading.range ?? pinned?.range;
|
|
285
|
+
if (range !== undefined) ref.range = range;
|
|
286
|
+
}
|
|
287
|
+
return ref;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Where a located reading's repository lives, as a request carries it. */
|
|
291
|
+
function remoteOf(
|
|
292
|
+
reading: RefReading,
|
|
293
|
+
written: string
|
|
294
|
+
): NonNullable<WorkItem["remote"]> {
|
|
295
|
+
const location = reading.location!;
|
|
296
|
+
return {
|
|
297
|
+
identity: location.identity,
|
|
298
|
+
url: location.url,
|
|
299
|
+
provider: location.provider,
|
|
300
|
+
written: written.trim(),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Whether a reading's repository is one the project added and its cached index
|
|
306
|
+
* publishes what the reading names. It reads only what is already here.
|
|
307
|
+
*
|
|
308
|
+
* @param reading - One reading of a dependency.
|
|
309
|
+
* @param context - The added repositories and their cached indexes.
|
|
310
|
+
*/
|
|
311
|
+
function knownLocally(reading: RefReading, context: ResolveContext): boolean {
|
|
312
|
+
if (reading.location === undefined) return false;
|
|
313
|
+
const added = repoNamedByIdentity(context.repos, reading.location.identity);
|
|
314
|
+
const index = added === undefined ? undefined : context.indexes.get(added);
|
|
315
|
+
if (index === undefined) return false;
|
|
316
|
+
return settleInIndex(reading, { namespaces: Object.keys(index.namespaces), recipes: index.recipes }).length > 0;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* The error for a dependency that reads more than one way with nothing to
|
|
321
|
+
* settle it.
|
|
322
|
+
*
|
|
323
|
+
* @param written - The dependency as written.
|
|
324
|
+
* @param parent - The recipe that declared it.
|
|
325
|
+
*/
|
|
326
|
+
function unsettledDependencyError(written: string, parent: ResolvedRecipe): ConfigError {
|
|
327
|
+
const readings = parseRef(written, RefSource.Manifest);
|
|
328
|
+
return new ConfigError(
|
|
329
|
+
`The dependency '${written.trim()}' of the recipe '${parent.key}' can be read ` +
|
|
330
|
+
`${readings.length} ways, and nothing says which one it means:\n` +
|
|
331
|
+
readings.map((reading) => ` ${formatRef(reading)}`).join("\n") +
|
|
332
|
+
`\n A release records which one it settled on in the index; the index of ` +
|
|
333
|
+
`'${parent.repo}' records none for this version. Release the recipe again with this ` +
|
|
334
|
+
`version of sous, or write the dependency so it reads one way, as one of the forms above.`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Settles a browsed path, once the project has the repository it names.
|
|
340
|
+
*
|
|
341
|
+
* @param item - The request, whose `ref` is filled in.
|
|
342
|
+
* @param index - The repository's cached index.
|
|
343
|
+
*/
|
|
344
|
+
function settleBrowsedItem(item: WorkItem, index: IndexFile | undefined): void {
|
|
345
|
+
const remote = item.remote!;
|
|
346
|
+
const settled =
|
|
347
|
+
index === undefined
|
|
348
|
+
? []
|
|
349
|
+
: settleBrowsed(remote.browsed!, {
|
|
350
|
+
namespaces: Object.keys(index.namespaces),
|
|
351
|
+
recipes: index.recipes,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
let found: ParsedRef | undefined = settled.length === 1 ? settled[0] : undefined;
|
|
355
|
+
if (found === undefined) {
|
|
356
|
+
// The folder may have moved since the release; the key it settled on then
|
|
357
|
+
// is recorded beside the repository it lives in.
|
|
358
|
+
const recorded = Object.keys(remote.pins ?? {}).filter(
|
|
359
|
+
(key) => remote.pins![key]!.repo === remote.identity
|
|
360
|
+
);
|
|
361
|
+
if (recorded.length === 1) {
|
|
362
|
+
const { namespace, name } = splitRecipeKey(recorded[0]!);
|
|
363
|
+
found = { namespace, recipe: name };
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (found === undefined) {
|
|
368
|
+
throw new ConfigError(
|
|
369
|
+
`The dependency '${remote.written}' names a folder, and the index of the repository at ` +
|
|
370
|
+
`${remote.url} ${settled.length === 0 ? "publishes no recipe there" : "publishes several recipes there"}.` +
|
|
371
|
+
`\n It was required by the recipe '${item.requestedBy}'. Write the dependency as the ` +
|
|
372
|
+
`recipe it means, such as '${remote.url}/<namespace>/<recipe>'.`
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const withRange = { ...found, ...(remote.range === undefined ? {} : { range: remote.range }) };
|
|
377
|
+
item.ref = { ...pinnedRef(withRange, remote.pins?.[refKey(found)]), repo: item.ref.repo };
|
|
378
|
+
}
|
|
379
|
+
|
|
244
380
|
/**
|
|
245
381
|
* The short name of the added repository with this identity, or undefined when
|
|
246
382
|
* the project has added none. Short names are a project's own labels, so the
|
|
@@ -249,7 +385,7 @@ function dependencyRequest(
|
|
|
249
385
|
* @param repos - The repositories the project has added.
|
|
250
386
|
* @param identity - The canonical identity to look for.
|
|
251
387
|
*/
|
|
252
|
-
function repoNamedByIdentity(
|
|
388
|
+
export function repoNamedByIdentity(
|
|
253
389
|
repos: Record<string, ResolverRepo>,
|
|
254
390
|
identity: string
|
|
255
391
|
): string | undefined {
|
|
@@ -266,7 +402,7 @@ function repoNamedByIdentity(
|
|
|
266
402
|
* @param repos - The repositories the project has added.
|
|
267
403
|
* @param identity - The canonical identity of the repository being named.
|
|
268
404
|
*/
|
|
269
|
-
function proposeRepoName(
|
|
405
|
+
export function proposeRepoName(
|
|
270
406
|
repos: Record<string, ResolverRepo>,
|
|
271
407
|
identity: string
|
|
272
408
|
): string {
|
|
@@ -334,6 +470,7 @@ export async function resolveRefs(
|
|
|
334
470
|
continue;
|
|
335
471
|
}
|
|
336
472
|
item.ref = { ...item.ref, repo: added };
|
|
473
|
+
if (item.remote.browsed !== undefined) settleBrowsedItem(item, context.indexes.get(added));
|
|
337
474
|
}
|
|
338
475
|
|
|
339
476
|
if (item.ref.recipe === undefined) {
|
|
@@ -369,7 +506,7 @@ export async function resolveRefs(
|
|
|
369
506
|
["subscribes", manifest.subscribes ?? []],
|
|
370
507
|
] as Array<[LockKind, string[]]>) {
|
|
371
508
|
for (const written of refs) {
|
|
372
|
-
queue.push(dependencyRequest(written, recipe, kind));
|
|
509
|
+
queue.push(dependencyRequest(written, recipe, kind, context));
|
|
373
510
|
}
|
|
374
511
|
}
|
|
375
512
|
}
|
|
@@ -473,20 +610,7 @@ async function keepOnlyReachable(
|
|
|
473
610
|
["subscribes", manifest.subscribes ?? []],
|
|
474
611
|
] as Array<[LockKind, string[]]>) {
|
|
475
612
|
for (const written of refs) {
|
|
476
|
-
|
|
477
|
-
try {
|
|
478
|
-
parsed = parseDependencyRef(written);
|
|
479
|
-
} catch {
|
|
480
|
-
continue;
|
|
481
|
-
}
|
|
482
|
-
// A namespace ref means every recipe in it, exactly as the walk expanded it.
|
|
483
|
-
const targets =
|
|
484
|
-
parsed.recipe === undefined
|
|
485
|
-
? [...resolved.keys()].filter((entry) =>
|
|
486
|
-
entry.startsWith(`${parsed.namespace}/`)
|
|
487
|
-
)
|
|
488
|
-
: [dependencyRefKey(parsed)];
|
|
489
|
-
for (const target of targets) {
|
|
613
|
+
for (const target of dependencyTargets(written, recipe, context, resolved)) {
|
|
490
614
|
if (!resolved.has(target)) continue;
|
|
491
615
|
if (declare(key, target, kind)) queue.push(target);
|
|
492
616
|
}
|
|
@@ -506,6 +630,39 @@ async function keepOnlyReachable(
|
|
|
506
630
|
}
|
|
507
631
|
}
|
|
508
632
|
|
|
633
|
+
/**
|
|
634
|
+
* The resolved keys one dependency of a recipe reaches, the same way the walk
|
|
635
|
+
* reached them: a namespace means every recipe in it, and a browsed path means
|
|
636
|
+
* the recipe it settles on. A dependency that no longer settles reaches
|
|
637
|
+
* nothing, because the walk would have failed on it.
|
|
638
|
+
*
|
|
639
|
+
* @param written - The dependency as the manifest wrote it.
|
|
640
|
+
* @param holder - The recipe that declared it.
|
|
641
|
+
* @param context - The added repositories and their cached indexes.
|
|
642
|
+
* @param resolved - What the walk resolved.
|
|
643
|
+
*/
|
|
644
|
+
function dependencyTargets(
|
|
645
|
+
written: string,
|
|
646
|
+
holder: ResolvedRecipe,
|
|
647
|
+
context: ResolveContext,
|
|
648
|
+
resolved: Map<string, ResolvedRecipe>
|
|
649
|
+
): string[] {
|
|
650
|
+
let item: WorkItem;
|
|
651
|
+
try {
|
|
652
|
+
item = dependencyRequest(written, holder, "depends", context);
|
|
653
|
+
if (item.remote?.browsed !== undefined) {
|
|
654
|
+
const added = repoNamedByIdentity(context.repos, item.remote.identity);
|
|
655
|
+
settleBrowsedItem(item, added === undefined ? undefined : context.indexes.get(added));
|
|
656
|
+
}
|
|
657
|
+
} catch {
|
|
658
|
+
return [];
|
|
659
|
+
}
|
|
660
|
+
const { namespace } = item.ref;
|
|
661
|
+
return item.ref.recipe === undefined
|
|
662
|
+
? [...resolved.keys()].filter((entry) => entry.startsWith(`${namespace}/`))
|
|
663
|
+
: [refKey(item.ref)];
|
|
664
|
+
}
|
|
665
|
+
|
|
509
666
|
/** Records one more reason a repository is needed. */
|
|
510
667
|
function recordMissingRepo(into: Map<string, MissingRepo>, missing: MissingRepo): void {
|
|
511
668
|
const existing = into.get(missing.name);
|
|
@@ -622,11 +779,11 @@ function resolveRecipeRef(
|
|
|
622
779
|
const requestedBy = [...(previous?.requestedBy ?? [])];
|
|
623
780
|
if (!requestedBy.includes(item.requestedBy)) requestedBy.push(item.requestedBy);
|
|
624
781
|
|
|
625
|
-
const namespace =
|
|
782
|
+
const { namespace, name } = splitRecipeKey(key);
|
|
626
783
|
return {
|
|
627
784
|
key,
|
|
628
785
|
namespace,
|
|
629
|
-
name
|
|
786
|
+
name,
|
|
630
787
|
repo: chosen.repo,
|
|
631
788
|
identity: context.repos[chosen.repo]?.identity ?? chosen.repo,
|
|
632
789
|
version,
|