@sous-io/sous 0.2.28 → 0.2.30
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 +2 -0
- package/docs/markdown/commands.md +10 -6
- package/docs/markdown/repositories-authoring.md +19 -11
- package/docs/markdown/repositories-consuming.md +34 -13
- package/docs/markdown/repositories-file-formats.md +88 -13
- package/docs/markdown/repositories-providers.md +8 -6
- package/docs/markdown/repositories-troubleshooting.md +6 -0
- package/docs/markdown/repositories-variables.md +13 -3
- 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/skills/about-sous-repos/SKILL.tpl.md +7 -4
- package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
- package/src/commands/lock/rebuild.ts +7 -6
- package/src/commands/recipe/show.ts +163 -64
- 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 +87 -80
- package/src/lib/repos/declarations.ts +177 -0
- package/src/lib/repos/formats/common.ts +14 -0
- package/src/lib/repos/formats/index-file.ts +42 -2
- package/src/lib/repos/formats/lockfile.ts +5 -3
- package/src/lib/repos/formats/recipe-manifest.ts +16 -212
- package/src/lib/repos/formats/variable-definition.ts +268 -0
- 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 +221 -47
- 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 +251 -53
- package/src/lib/repos/subscription-service.ts +355 -49
- package/src/lib/repos/update-plan.ts +3 -2
- package/src/lib/vars/mappings.ts +17 -30
- package/src/lib/vars/question-plan.ts +44 -21
- package/src/lib/repos/ref.ts +0 -513
|
@@ -56,20 +56,33 @@ import {
|
|
|
56
56
|
type LockedRecipe,
|
|
57
57
|
type Lockfile,
|
|
58
58
|
} from "./formats/lockfile.js";
|
|
59
|
-
import type { RecipeManifest } from "./formats/recipe-manifest.js";
|
|
60
|
-
import { formatRef, parseRef, refKey, type ParsedRef } from "./ref.js";
|
|
59
|
+
import type { RecipeManifest, VariableDefinition } from "./formats/recipe-manifest.js";
|
|
61
60
|
import {
|
|
61
|
+
RefSource,
|
|
62
|
+
Qualification,
|
|
62
63
|
SousScope,
|
|
64
|
+
formatRef,
|
|
65
|
+
isNamedReading,
|
|
66
|
+
matchNames,
|
|
67
|
+
namespaceOfKey,
|
|
68
|
+
parseRef,
|
|
69
|
+
parseShortRef,
|
|
70
|
+
refKey,
|
|
71
|
+
splitRecipeKey,
|
|
72
|
+
type ParsedRef,
|
|
73
|
+
type RefReading,
|
|
63
74
|
describeReference,
|
|
64
75
|
findReference,
|
|
65
76
|
pickReference,
|
|
66
77
|
referenceReposFromIndexes,
|
|
67
78
|
referenceToRef,
|
|
68
79
|
type ReferenceMatch,
|
|
80
|
+
type ReferenceRepo,
|
|
69
81
|
} from "../refs/index.js";
|
|
70
82
|
import { describeIndexSearch } from "./ref-search.js";
|
|
71
83
|
import {
|
|
72
84
|
PROJECT_REQUESTER,
|
|
85
|
+
proposeRepoName,
|
|
73
86
|
resolveRefs,
|
|
74
87
|
type MissingRepo,
|
|
75
88
|
type RefRequest,
|
|
@@ -279,9 +292,9 @@ export type SubscribeOutcome = {
|
|
|
279
292
|
*/
|
|
280
293
|
questions?: PlannedVariable[];
|
|
281
294
|
/**
|
|
282
|
-
* Recipes a dry run could not describe
|
|
283
|
-
*
|
|
284
|
-
* they are installed.
|
|
295
|
+
* Recipes a dry run could not describe: their files are not on this machine,
|
|
296
|
+
* a dry run downloads nothing, and their repository's index does not record
|
|
297
|
+
* their questions. Their questions are unknown until they are installed.
|
|
285
298
|
*/
|
|
286
299
|
unreadable?: string[];
|
|
287
300
|
/** Dependency cycles the resolver noticed, reported rather than treated as fatal. */
|
|
@@ -290,6 +303,28 @@ export type SubscribeOutcome = {
|
|
|
290
303
|
dryRun: boolean;
|
|
291
304
|
};
|
|
292
305
|
|
|
306
|
+
/** What `previewSubscription` is asked about. */
|
|
307
|
+
export type PreviewSubscriptionOptions = {
|
|
308
|
+
/** The indexes to resolve against, keyed by short name; the cached ones by default. */
|
|
309
|
+
indexes?: Map<string, IndexFile>;
|
|
310
|
+
/** Whether prerelease versions may take part in range matching. */
|
|
311
|
+
prerelease?: boolean;
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
/** What subscribing to a ref would install and ask, worked out without changing anything. */
|
|
315
|
+
export type SubscriptionPreview = {
|
|
316
|
+
/** Every recipe version the subscription would install, sorted by key. */
|
|
317
|
+
resolved: ResolvedRecipe[];
|
|
318
|
+
/** Repositories the closure needs that the project does not trust yet. */
|
|
319
|
+
missingRepos: MissingRepo[];
|
|
320
|
+
/** Every question the closure would ask, and where each answer would go. */
|
|
321
|
+
questions: PlannedVariable[];
|
|
322
|
+
/** Recipes whose questions cannot be listed; see `SubscribeOutcome.unreadable`. */
|
|
323
|
+
unreadable: string[];
|
|
324
|
+
/** Dependency cycles the resolver noticed. */
|
|
325
|
+
cycles: string[][];
|
|
326
|
+
};
|
|
327
|
+
|
|
293
328
|
/** What `unsubscribe` is asked to do. */
|
|
294
329
|
export type UnsubscribeOptions = {
|
|
295
330
|
/** The ref to unsubscribe from. */
|
|
@@ -639,6 +674,26 @@ export class SubscriptionService {
|
|
|
639
674
|
return identity === undefined ? undefined : this.indexCache.readCached(identity);
|
|
640
675
|
}
|
|
641
676
|
|
|
677
|
+
/**
|
|
678
|
+
* Every trusted repository as a reference searches it, read from the cached
|
|
679
|
+
* indexes alone: what it publishes, where each recipe lives, and its
|
|
680
|
+
* canonical identity, so a reference written as a location finds it whatever
|
|
681
|
+
* this project calls it. A repository whose index has never been fetched can
|
|
682
|
+
* still be named, just not searched inside.
|
|
683
|
+
*/
|
|
684
|
+
cachedReferenceRepos(): ReferenceRepo[] {
|
|
685
|
+
const repos = this.resolverRepos();
|
|
686
|
+
return Object.keys(repos).map((name) => {
|
|
687
|
+
const { url, identity } = repos[name]!;
|
|
688
|
+
const index = this.cachedIndex(name);
|
|
689
|
+
const base =
|
|
690
|
+
index === undefined
|
|
691
|
+
? { name, url, namespaces: [], recipes: [] }
|
|
692
|
+
: referenceReposFromIndexes([name], new Map([[name, index]]), { [name]: url })[0]!;
|
|
693
|
+
return { ...base, identity };
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
642
697
|
// --- Repository identity ----------------------------------------------------------------------
|
|
643
698
|
|
|
644
699
|
/**
|
|
@@ -796,27 +851,30 @@ export class SubscriptionService {
|
|
|
796
851
|
* @param options - The ref, the prerelease and always-pull flags, and the trust flag.
|
|
797
852
|
*/
|
|
798
853
|
async subscribe(options: SubscribeOptions): Promise<SubscribeOutcome> {
|
|
799
|
-
const written = parseRef(options.ref);
|
|
800
854
|
const dryRun = options.dryRun === true;
|
|
801
855
|
|
|
802
|
-
//
|
|
803
|
-
//
|
|
804
|
-
//
|
|
805
|
-
|
|
856
|
+
// What the ref names is settled here, from the cached indexes alone: a
|
|
857
|
+
// one-word ref is a guess at a name, and a location is matched against the
|
|
858
|
+
// repository at that location (trusted first, when it is not yet). Every
|
|
859
|
+
// step after this works with a fully qualified ref, so what is confirmed
|
|
860
|
+
// is exactly what is installed.
|
|
861
|
+
const { parsed, trusted: trustedForRef } = await this.resolveWrittenRef(options.ref, options);
|
|
806
862
|
const key = refKey(parsed);
|
|
807
863
|
|
|
808
864
|
// The last gate before anything is fetched or written: what this will do to
|
|
809
865
|
// the project, in plain sentences, and a question.
|
|
810
866
|
await this.confirmSubscription(parsed, options);
|
|
811
867
|
|
|
812
|
-
const
|
|
868
|
+
const closure = await this.resolveClosure(parsed, options);
|
|
869
|
+
const { resolved, cycles } = closure;
|
|
870
|
+
const trusted = [...trustedForRef, ...closure.trusted];
|
|
813
871
|
|
|
814
872
|
const before = this.lock.read();
|
|
815
873
|
const after = this.lock.applyResolution(before, resolved, this.lockRepoInputs());
|
|
816
874
|
const diff = this.lock.diff(before, after);
|
|
817
875
|
|
|
818
876
|
const resolvedFrom =
|
|
819
|
-
|
|
877
|
+
options.ref.trim() === formatRef(parsed) ? {} : { resolvedFrom: options.ref.trim() };
|
|
820
878
|
|
|
821
879
|
if (dryRun) {
|
|
822
880
|
// The questions are planned even here, so `--dry-run` is the command an
|
|
@@ -881,6 +939,161 @@ export class SubscriptionService {
|
|
|
881
939
|
|
|
882
940
|
// --- Working out what a one-word ref meant -----------------------------------------------------
|
|
883
941
|
|
|
942
|
+
/**
|
|
943
|
+
* Settles what a ref typed on the command line names, in any form it was
|
|
944
|
+
* written: a short ref goes through `resolveBareRef`; a location (a URL, an
|
|
945
|
+
* SSH remote, a provider-scheme locator, a browser URL) is matched against
|
|
946
|
+
* the trusted repository at that location and settled through its index.
|
|
947
|
+
* When no reading's repository is trusted yet, the trust ceremony runs for
|
|
948
|
+
* it first, exactly as it runs for a dependency's repository, and nothing is
|
|
949
|
+
* fetched from it before the answer.
|
|
950
|
+
*
|
|
951
|
+
* @param written - The ref exactly as the user wrote it.
|
|
952
|
+
* @param options - The trust and accept-first flags.
|
|
953
|
+
* @returns The fully qualified ref, and any repository trusted to settle it.
|
|
954
|
+
*/
|
|
955
|
+
private async resolveWrittenRef(
|
|
956
|
+
written: string,
|
|
957
|
+
options: SubscribeOptions
|
|
958
|
+
): Promise<{ parsed: ParsedRef; trusted: string[] }> {
|
|
959
|
+
const readings = parseRef(written, RefSource.CommandLine, this.providers);
|
|
960
|
+
const first = readings[0]!;
|
|
961
|
+
if (first.location === undefined && isNamedReading(first)) {
|
|
962
|
+
return { parsed: await this.resolveBareRef(first, options), trusted: [] };
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const trusted = await this.trustLocation(written, readings, options);
|
|
966
|
+
const chosen = await this.pickLocated(written, options.acceptFirst);
|
|
967
|
+
const range = "range" in first ? first.range : undefined;
|
|
968
|
+
return {
|
|
969
|
+
parsed: referenceToRef(chosen, {
|
|
970
|
+
namespace: chosen.namespace!,
|
|
971
|
+
...(range === undefined ? {} : { range }),
|
|
972
|
+
}),
|
|
973
|
+
trusted,
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* The one namespace or recipe a location names among the trusted
|
|
979
|
+
* repositories, settled through their cached indexes and chosen between the
|
|
980
|
+
* way every reference is.
|
|
981
|
+
*
|
|
982
|
+
* @param written - The location as written.
|
|
983
|
+
* @param acceptFirst - Whether `--accept-first` was passed.
|
|
984
|
+
*/
|
|
985
|
+
private async pickLocated(written: string, acceptFirst?: boolean): Promise<ReferenceMatch> {
|
|
986
|
+
const repoOrder = this.repoSearchOrder();
|
|
987
|
+
const indexes = await this.loadIndexes(repoOrder);
|
|
988
|
+
this.indexesSnapshot = indexes;
|
|
989
|
+
|
|
990
|
+
const matches = findReference(
|
|
991
|
+
written,
|
|
992
|
+
[SousScope.Namespace, SousScope.Recipe],
|
|
993
|
+
{ repos: this.referenceRepos(repoOrder, indexes) }
|
|
994
|
+
);
|
|
995
|
+
|
|
996
|
+
return pickReference(matches, {
|
|
997
|
+
search: written.trim(),
|
|
998
|
+
interactive: this.interactive,
|
|
999
|
+
...(acceptFirst === undefined ? {} : { acceptFirst }),
|
|
1000
|
+
details: [
|
|
1001
|
+
` The repository at that location is trusted, but its index publishes no namespace ` +
|
|
1002
|
+
`or recipe there.`,
|
|
1003
|
+
` Run 'sous repo list --latest' to read what it publishes now.`,
|
|
1004
|
+
],
|
|
1005
|
+
write: (message: string) => this.write(message),
|
|
1006
|
+
choose: (message, offered) => this.choose(message, offered),
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* The trusted repositories as a reference searches them, each carrying its
|
|
1012
|
+
* canonical identity so a location finds it whatever this project calls it.
|
|
1013
|
+
*
|
|
1014
|
+
* @param repoOrder - The repositories, in search order.
|
|
1015
|
+
* @param indexes - Their indexes.
|
|
1016
|
+
*/
|
|
1017
|
+
private referenceRepos(repoOrder: string[], indexes: Map<string, IndexFile>) {
|
|
1018
|
+
const repos = this.resolverRepos();
|
|
1019
|
+
const urls: Record<string, string | undefined> = {};
|
|
1020
|
+
for (const name of repoOrder) urls[name] = repos[name]?.url;
|
|
1021
|
+
return referenceReposFromIndexes(repoOrder, indexes, urls).map((repo) => ({
|
|
1022
|
+
...repo,
|
|
1023
|
+
...(repos[repo.name] === undefined ? {} : { identity: repos[repo.name]!.identity }),
|
|
1024
|
+
}));
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* Makes sure the repository a located ref names is trusted: nothing happens
|
|
1029
|
+
* when any reading's repository already is. Otherwise the one repository the
|
|
1030
|
+
* location names goes through the trust ceremony; a location that reads as
|
|
1031
|
+
* several repositories (a GitLab URL with nested groups) is first a question
|
|
1032
|
+
* of which one, since nothing may be fetched from a repository to find out
|
|
1033
|
+
* before it is trusted.
|
|
1034
|
+
*
|
|
1035
|
+
* @param written - The location as written.
|
|
1036
|
+
* @param readings - Every reading of it.
|
|
1037
|
+
* @param options - The trust and accept-first flags.
|
|
1038
|
+
* @returns The short names of the repositories trusted here.
|
|
1039
|
+
*/
|
|
1040
|
+
private async trustLocation(
|
|
1041
|
+
written: string,
|
|
1042
|
+
readings: RefReading[],
|
|
1043
|
+
options: SubscribeOptions
|
|
1044
|
+
): Promise<string[]> {
|
|
1045
|
+
const repos = this.resolverRepos();
|
|
1046
|
+
const known = new Set(Object.values(repos).map((repo) => repo.identity));
|
|
1047
|
+
const located = readings.filter((reading) => reading.location !== undefined);
|
|
1048
|
+
if (located.some((reading) => known.has(reading.location!.identity))) return [];
|
|
1049
|
+
|
|
1050
|
+
let chosen = located[0]!;
|
|
1051
|
+
const identities = new Set(located.map((reading) => reading.location!.identity));
|
|
1052
|
+
if (identities.size > 1) {
|
|
1053
|
+
const offered: ReferenceMatch[] = located.map((reading) => ({
|
|
1054
|
+
scope:
|
|
1055
|
+
isNamedReading(reading) && reading.recipe !== undefined
|
|
1056
|
+
? SousScope.Recipe
|
|
1057
|
+
: SousScope.Namespace,
|
|
1058
|
+
key: formatRef(reading, this.providers),
|
|
1059
|
+
label: formatRef(reading, this.providers),
|
|
1060
|
+
repo: reading.location!.url,
|
|
1061
|
+
...(isNamedReading(reading) ? { namespace: reading.namespace } : {}),
|
|
1062
|
+
...(isNamedReading(reading) && reading.recipe !== undefined
|
|
1063
|
+
? { recipe: reading.recipe }
|
|
1064
|
+
: {}),
|
|
1065
|
+
qualification: Qualification.Full,
|
|
1066
|
+
}));
|
|
1067
|
+
const picked = await pickReference(offered, {
|
|
1068
|
+
search: written.trim(),
|
|
1069
|
+
interactive: this.interactive,
|
|
1070
|
+
...(options.acceptFirst === undefined ? {} : { acceptFirst: options.acceptFirst }),
|
|
1071
|
+
prompt: `'${written.trim()}' can be read as more than one repository. Which one?`,
|
|
1072
|
+
write: (message: string) => this.write(message),
|
|
1073
|
+
choose: (message, list) => this.choose(message, list),
|
|
1074
|
+
});
|
|
1075
|
+
chosen = located[offered.indexOf(picked)]!;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
const location = chosen.location!;
|
|
1079
|
+
const outcome = await this.trust.confirmTrust(
|
|
1080
|
+
[
|
|
1081
|
+
{
|
|
1082
|
+
name: proposeRepoName(repos, location.identity),
|
|
1083
|
+
url: location.url,
|
|
1084
|
+
identity: location.identity,
|
|
1085
|
+
provider: location.provider,
|
|
1086
|
+
requiredBy: [{ ref: written.trim(), requestedBy: PROJECT_REQUESTER }],
|
|
1087
|
+
},
|
|
1088
|
+
],
|
|
1089
|
+
{
|
|
1090
|
+
interactive: this.interactive,
|
|
1091
|
+
...(options.trust === undefined ? {} : { trustFlag: options.trust }),
|
|
1092
|
+
}
|
|
1093
|
+
);
|
|
1094
|
+
return outcome.added;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
884
1097
|
/**
|
|
885
1098
|
* Settles what a ref names, reading nothing but the cached indexes.
|
|
886
1099
|
*
|
|
@@ -898,33 +1111,39 @@ export class SubscriptionService {
|
|
|
898
1111
|
written: ParsedRef,
|
|
899
1112
|
options: SubscribeOptions
|
|
900
1113
|
): Promise<ParsedRef> {
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
const
|
|
1114
|
+
// A ref that names a recipe, spelled as it is published, says what it
|
|
1115
|
+
// names already. One word, or a spelling in another case, is searched.
|
|
1116
|
+
const search =
|
|
1117
|
+
written.repo === undefined ? refKey(written) : `${written.repo}:${refKey(written)}`;
|
|
1118
|
+
if (written.recipe !== undefined && refKey(written) === refKey(written).toLowerCase()) {
|
|
1119
|
+
return written;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
const qualifier =
|
|
1123
|
+
written.repo === undefined
|
|
1124
|
+
? undefined
|
|
1125
|
+
: (matchNames(written.repo, Object.keys(this.currentRepos()))[0] ?? written.repo);
|
|
1126
|
+
const repoOrder = this.repoSearchOrder(qualifier);
|
|
904
1127
|
const indexes = await this.loadIndexes(repoOrder);
|
|
905
1128
|
if (written.repo === undefined) this.indexesSnapshot = indexes;
|
|
906
1129
|
|
|
907
1130
|
const context = { repos: referenceReposFromIndexes(repoOrder, indexes) };
|
|
908
|
-
const matches = findReference(
|
|
909
|
-
written.namespace,
|
|
910
|
-
[SousScope.Namespace, SousScope.Recipe],
|
|
911
|
-
context
|
|
912
|
-
);
|
|
1131
|
+
const matches = findReference(search, [SousScope.Namespace, SousScope.Recipe], context);
|
|
913
1132
|
|
|
914
1133
|
if (matches.length === 0) {
|
|
915
1134
|
throw new ConfigError(
|
|
916
1135
|
[
|
|
917
|
-
`Nothing called '${written
|
|
1136
|
+
`Nothing called '${refKey(written)}' was found: no namespace has that name, ` +
|
|
918
1137
|
`and no recipe does either.`,
|
|
919
|
-
...describeIndexSearch({ name: written
|
|
920
|
-
` Run 'sous repo search ${written
|
|
1138
|
+
...describeIndexSearch({ name: refKey(written), repoOrder, indexes }),
|
|
1139
|
+
` Run 'sous repo search ${refKey(written)}' to look for something like it, or ` +
|
|
921
1140
|
`'sous repo add <url>' to add the repository that publishes it.`,
|
|
922
1141
|
].join("\n")
|
|
923
1142
|
);
|
|
924
1143
|
}
|
|
925
1144
|
|
|
926
1145
|
const chosen = await pickReference(matches, {
|
|
927
|
-
search
|
|
1146
|
+
search,
|
|
928
1147
|
interactive: this.interactive,
|
|
929
1148
|
...(options.acceptFirst === undefined ? {} : { acceptFirst: options.acceptFirst }),
|
|
930
1149
|
write: (message: string) => this.write(message),
|
|
@@ -1272,6 +1491,26 @@ export class SubscriptionService {
|
|
|
1272
1491
|
}
|
|
1273
1492
|
}
|
|
1274
1493
|
|
|
1494
|
+
/**
|
|
1495
|
+
* The subscription key a ref typed on the command line names. A short ref
|
|
1496
|
+
* names its own key, matched against the keys in force exactly first and then
|
|
1497
|
+
* ignoring case; a location is settled through the trusted repository at that
|
|
1498
|
+
* location, the way `subscribe` settles one.
|
|
1499
|
+
*
|
|
1500
|
+
* @param written - The ref as typed.
|
|
1501
|
+
* @param keys - The subscription keys in force.
|
|
1502
|
+
*/
|
|
1503
|
+
private async subscriptionKeyFor(written: string, keys: string[]): Promise<string> {
|
|
1504
|
+
const readings = parseRef(written, RefSource.CommandLine, this.providers);
|
|
1505
|
+
const first = readings[0]!;
|
|
1506
|
+
if (first.location === undefined && isNamedReading(first)) {
|
|
1507
|
+
const key = refKey(first);
|
|
1508
|
+
return matchNames(key, new Set(keys))[0] ?? key;
|
|
1509
|
+
}
|
|
1510
|
+
const chosen = await this.pickLocated(written);
|
|
1511
|
+
return refKey(referenceToRef(chosen, { namespace: chosen.namespace! }));
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1275
1514
|
/**
|
|
1276
1515
|
* Removes one subscription and everything that was only there because of it.
|
|
1277
1516
|
* Removal is refcounted: a recipe another subscription (or another recipe)
|
|
@@ -1280,12 +1519,14 @@ export class SubscriptionService {
|
|
|
1280
1519
|
* @param options - The ref to unsubscribe from.
|
|
1281
1520
|
*/
|
|
1282
1521
|
async unsubscribe(options: UnsubscribeOptions): Promise<UnsubscribeOutcome> {
|
|
1283
|
-
const parsed = parseRef(options.ref);
|
|
1284
|
-
const key = refKey(parsed);
|
|
1285
1522
|
const dryRun = options.dryRun === true;
|
|
1286
1523
|
|
|
1287
1524
|
const managed = this.readSubscriptionEntries();
|
|
1288
1525
|
const configured = enabledSubscriptions(this.settings);
|
|
1526
|
+
const key = await this.subscriptionKeyFor(options.ref, [
|
|
1527
|
+
...Object.keys(managed),
|
|
1528
|
+
...Object.keys(configured),
|
|
1529
|
+
]);
|
|
1289
1530
|
const before = this.lock.read();
|
|
1290
1531
|
const held = this.keysHeldBySubscription(before, key);
|
|
1291
1532
|
|
|
@@ -1521,7 +1762,7 @@ export class SubscriptionService {
|
|
|
1521
1762
|
for (const key of Object.keys(subscriptions).sort()) {
|
|
1522
1763
|
let qualifier: string | undefined;
|
|
1523
1764
|
try {
|
|
1524
|
-
qualifier =
|
|
1765
|
+
qualifier = parseShortRef(key, RefSource.Config).repo;
|
|
1525
1766
|
} catch {
|
|
1526
1767
|
// A ref that does not parse cannot name this repository, and reporting
|
|
1527
1768
|
// it here would bury the removal under an unrelated complaint.
|
|
@@ -1974,7 +2215,7 @@ export class SubscriptionService {
|
|
|
1974
2215
|
const entry = subscriptions[key]!;
|
|
1975
2216
|
let request: RefRequest;
|
|
1976
2217
|
try {
|
|
1977
|
-
const parsed =
|
|
2218
|
+
const parsed = parseShortRef(key, RefSource.Config);
|
|
1978
2219
|
request = {
|
|
1979
2220
|
ref: { ...parsed, ...(entry.range === undefined ? {} : { range: entry.range }) },
|
|
1980
2221
|
requestedBy: PROJECT_REQUESTER,
|
|
@@ -2082,7 +2323,7 @@ export class SubscriptionService {
|
|
|
2082
2323
|
|
|
2083
2324
|
let parsed: ParsedRef;
|
|
2084
2325
|
try {
|
|
2085
|
-
parsed =
|
|
2326
|
+
parsed = parseShortRef(key, RefSource.Config);
|
|
2086
2327
|
} catch {
|
|
2087
2328
|
return false;
|
|
2088
2329
|
}
|
|
@@ -2127,7 +2368,7 @@ export class SubscriptionService {
|
|
|
2127
2368
|
): Lockfile {
|
|
2128
2369
|
const coverOf = (key: string): string | undefined => {
|
|
2129
2370
|
try {
|
|
2130
|
-
return refKey(
|
|
2371
|
+
return refKey(parseShortRef(key, RefSource.Config));
|
|
2131
2372
|
} catch {
|
|
2132
2373
|
return undefined;
|
|
2133
2374
|
}
|
|
@@ -2217,7 +2458,7 @@ export class SubscriptionService {
|
|
|
2217
2458
|
if (scope.kind === "all") return true;
|
|
2218
2459
|
let parsed: ParsedRef;
|
|
2219
2460
|
try {
|
|
2220
|
-
parsed =
|
|
2461
|
+
parsed = parseShortRef(key, RefSource.Config);
|
|
2221
2462
|
} catch {
|
|
2222
2463
|
return false;
|
|
2223
2464
|
}
|
|
@@ -2311,7 +2552,7 @@ export class SubscriptionService {
|
|
|
2311
2552
|
if (entry.repo !== repoName) continue;
|
|
2312
2553
|
const range = this.effectiveRangeFor(key, entry, subscriptions);
|
|
2313
2554
|
if (range === undefined) continue;
|
|
2314
|
-
const subscription = subscriptions[key] ?? subscriptions[key
|
|
2555
|
+
const subscription = subscriptions[key] ?? subscriptions[namespaceOfKey(key)];
|
|
2315
2556
|
|
|
2316
2557
|
const found = findNewerInRange({
|
|
2317
2558
|
index,
|
|
@@ -2403,7 +2644,7 @@ export class SubscriptionService {
|
|
|
2403
2644
|
|
|
2404
2645
|
let parsed: ParsedRef;
|
|
2405
2646
|
try {
|
|
2406
|
-
parsed =
|
|
2647
|
+
parsed = parseShortRef(key, RefSource.Config);
|
|
2407
2648
|
} catch (error) {
|
|
2408
2649
|
report.failed.push({ key, reason: describeError(error) });
|
|
2409
2650
|
continue;
|
|
@@ -2670,7 +2911,7 @@ export class SubscriptionService {
|
|
|
2670
2911
|
|
|
2671
2912
|
for (const [key, entry] of Object.entries(recipes)) {
|
|
2672
2913
|
if (entry.repo !== repoName) continue;
|
|
2673
|
-
const subscription = subscriptions[key] ?? subscriptions[key
|
|
2914
|
+
const subscription = subscriptions[key] ?? subscriptions[namespaceOfKey(key)];
|
|
2674
2915
|
|
|
2675
2916
|
// Always-pull re-resolves WITHIN what was declared; it never widens it.
|
|
2676
2917
|
// A recipe held only through another recipe's `depends` has no
|
|
@@ -3062,11 +3303,11 @@ export class SubscriptionService {
|
|
|
3062
3303
|
const provider = requireProvider(request.url, request.providerId, this.providers);
|
|
3063
3304
|
const canonical = provider.canonicalize(request.url);
|
|
3064
3305
|
|
|
3065
|
-
const namespace =
|
|
3306
|
+
const { namespace, name } = splitRecipeKey(request.key);
|
|
3066
3307
|
const key: StoreKey = {
|
|
3067
3308
|
identity: request.identity,
|
|
3068
3309
|
namespace,
|
|
3069
|
-
name
|
|
3310
|
+
name,
|
|
3070
3311
|
version: request.version,
|
|
3071
3312
|
};
|
|
3072
3313
|
|
|
@@ -3231,7 +3472,7 @@ export class SubscriptionService {
|
|
|
3231
3472
|
entry: LockedRecipe,
|
|
3232
3473
|
subscriptions: Record<string, SubscriptionEntry>
|
|
3233
3474
|
): string | undefined {
|
|
3234
|
-
const namespace = key
|
|
3475
|
+
const namespace = namespaceOfKey(key);
|
|
3235
3476
|
return effectiveRangeForHolders(key, entry.requestedBy, {
|
|
3236
3477
|
subscriptionRange: (held) => {
|
|
3237
3478
|
// A subscription the config no longer declares is not a hold sous can
|
|
@@ -3270,15 +3511,20 @@ export class SubscriptionService {
|
|
|
3270
3511
|
if (manifest === undefined) return undefined;
|
|
3271
3512
|
|
|
3272
3513
|
for (const dependency of manifest.depends ?? []) {
|
|
3273
|
-
|
|
3514
|
+
// Every reading counts: a dependency that reads more than one way still
|
|
3515
|
+
// declares its range for whichever reading the lockfile settled on.
|
|
3516
|
+
let readings: RefReading[];
|
|
3274
3517
|
try {
|
|
3275
|
-
|
|
3518
|
+
readings = parseRef(dependency, RefSource.Manifest, this.providers);
|
|
3276
3519
|
} catch {
|
|
3277
3520
|
continue;
|
|
3278
3521
|
}
|
|
3279
|
-
const
|
|
3280
|
-
|
|
3281
|
-
|
|
3522
|
+
for (const reading of readings) {
|
|
3523
|
+
if (!isNamedReading(reading)) continue;
|
|
3524
|
+
const dependencyKey = refKey(reading);
|
|
3525
|
+
if (dependencyKey !== key && dependencyKey !== namespace) continue;
|
|
3526
|
+
return reading.range ?? "*";
|
|
3527
|
+
}
|
|
3282
3528
|
}
|
|
3283
3529
|
|
|
3284
3530
|
return undefined;
|
|
@@ -3305,9 +3551,10 @@ export class SubscriptionService {
|
|
|
3305
3551
|
* Every variable definition the resolved closure publishes, attributed to the
|
|
3306
3552
|
* recipe that declared it and to the chain that pulled it in.
|
|
3307
3553
|
*
|
|
3308
|
-
* A recipe whose files are not on this machine
|
|
3309
|
-
*
|
|
3310
|
-
*
|
|
3554
|
+
* A recipe whose files are not on this machine is described from the
|
|
3555
|
+
* definitions its index entry records, and contributes nothing rather than
|
|
3556
|
+
* failing when the entry records none, which is what lets a dry run describe
|
|
3557
|
+
* as much of the closure as it can without downloading any of it.
|
|
3311
3558
|
*
|
|
3312
3559
|
* @param resolved - The recipe versions the resolution settled on.
|
|
3313
3560
|
*/
|
|
@@ -3357,12 +3604,12 @@ export class SubscriptionService {
|
|
|
3357
3604
|
});
|
|
3358
3605
|
|
|
3359
3606
|
for (const recipe of ordered) {
|
|
3360
|
-
const
|
|
3361
|
-
if (
|
|
3607
|
+
const variables = this.publishedVariables(recipe);
|
|
3608
|
+
if (variables === undefined) continue;
|
|
3362
3609
|
|
|
3363
3610
|
const publisher = describe(recipe);
|
|
3364
3611
|
const requiredBy = chainFor(recipe).map(describe);
|
|
3365
|
-
for (const definition of
|
|
3612
|
+
for (const definition of variables) {
|
|
3366
3613
|
defined.push({ definition, recipe: publisher, requiredBy });
|
|
3367
3614
|
}
|
|
3368
3615
|
}
|
|
@@ -3380,11 +3627,70 @@ export class SubscriptionService {
|
|
|
3380
3627
|
*/
|
|
3381
3628
|
private unreadableRecipes(resolved: ResolvedRecipe[]): string[] {
|
|
3382
3629
|
return resolved
|
|
3383
|
-
.filter((recipe) =>
|
|
3630
|
+
.filter((recipe) => this.publishedVariables(recipe) === undefined)
|
|
3384
3631
|
.map((recipe) => recipe.key)
|
|
3385
3632
|
.sort();
|
|
3386
3633
|
}
|
|
3387
3634
|
|
|
3635
|
+
/**
|
|
3636
|
+
* The variable definitions one resolved recipe publishes: from its manifest
|
|
3637
|
+
* when its files are on this machine (a linked checkout's working copy
|
|
3638
|
+
* included), otherwise from its index entry, or undefined when neither can
|
|
3639
|
+
* say.
|
|
3640
|
+
*
|
|
3641
|
+
* @param recipe - The resolved recipe.
|
|
3642
|
+
*/
|
|
3643
|
+
private publishedVariables(recipe: ResolvedRecipe): VariableDefinition[] | undefined {
|
|
3644
|
+
const manifest = readRecipeManifestIn(this.recipeDirectory(recipe));
|
|
3645
|
+
if (manifest !== undefined) return manifest.variables ?? [];
|
|
3646
|
+
return recipe.variables;
|
|
3647
|
+
}
|
|
3648
|
+
|
|
3649
|
+
/**
|
|
3650
|
+
* Works out what subscribing to a ref would install and which questions it
|
|
3651
|
+
* would ask, from the indexes and the files already on this machine. Nothing
|
|
3652
|
+
* is fetched, trusted or written, which is what lets `sous recipe show`
|
|
3653
|
+
* describe a recipe before anyone subscribes to it. A repository the closure
|
|
3654
|
+
* needs and the project does not trust is reported, and its recipes are not
|
|
3655
|
+
* resolved.
|
|
3656
|
+
*
|
|
3657
|
+
* @param parsed - The fully qualified ref, with the exact version to describe as its range.
|
|
3658
|
+
* @param options - The indexes to read, and whether prereleases may match.
|
|
3659
|
+
*/
|
|
3660
|
+
async previewSubscription(
|
|
3661
|
+
parsed: ParsedRef,
|
|
3662
|
+
options: PreviewSubscriptionOptions = {}
|
|
3663
|
+
): Promise<SubscriptionPreview> {
|
|
3664
|
+
const repos = this.resolverRepos();
|
|
3665
|
+
const indexes = options.indexes ?? (await this.loadIndexes(Object.keys(repos)));
|
|
3666
|
+
const result = await resolveRefs(
|
|
3667
|
+
[
|
|
3668
|
+
{
|
|
3669
|
+
ref: parsed,
|
|
3670
|
+
requestedBy: PROJECT_REQUESTER,
|
|
3671
|
+
kind: "subscribes",
|
|
3672
|
+
...(options.prerelease === true ? { prerelease: true } : {}),
|
|
3673
|
+
},
|
|
3674
|
+
],
|
|
3675
|
+
{
|
|
3676
|
+
indexes,
|
|
3677
|
+
repos,
|
|
3678
|
+
loadManifest: (recipe) => this.loadRecipeManifest(recipe, true),
|
|
3679
|
+
...(options.prerelease === true ? { prerelease: true } : {}),
|
|
3680
|
+
}
|
|
3681
|
+
);
|
|
3682
|
+
|
|
3683
|
+
return {
|
|
3684
|
+
resolved: result.resolved,
|
|
3685
|
+
missingRepos: result.missingRepos,
|
|
3686
|
+
questions: planQuestions(this.definedVariables(result.resolved), this.ladderContext(), {
|
|
3687
|
+
sousDir: this.sousDir,
|
|
3688
|
+
}),
|
|
3689
|
+
unreadable: this.unreadableRecipes(result.resolved),
|
|
3690
|
+
cycles: result.cycles,
|
|
3691
|
+
};
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3388
3694
|
/** The environment layers and mapping records this project resolves against. */
|
|
3389
3695
|
private ladderContext(): LadderContext {
|
|
3390
3696
|
return loadLadderContext({
|
|
@@ -31,7 +31,7 @@ export type UpdatePlanFacts = {
|
|
|
31
31
|
missingRepos: MissingRepo[];
|
|
32
32
|
/** The questions the new versions ask that nothing answers yet. */
|
|
33
33
|
questions: PlannedVariable[];
|
|
34
|
-
/** Recipes whose files are not on this machine
|
|
34
|
+
/** Recipes whose files are not on this machine and whose index does not record their questions. */
|
|
35
35
|
unreadable: string[];
|
|
36
36
|
/** Repositories whose index could not be fetched, so their pins stay where they are. */
|
|
37
37
|
unreachable: Array<{ repo: string; reason: string }>;
|
|
@@ -153,7 +153,8 @@ export function formatUpdatePlan(
|
|
|
153
153
|
if (facts.unreadable.length > 0) {
|
|
154
154
|
notes.push(
|
|
155
155
|
`A dry run downloads no recipe, so the dependencies of the versions not on this ` +
|
|
156
|
-
`machine yet are not shown:
|
|
156
|
+
`machine yet, and not described by their index, are not shown: ` +
|
|
157
|
+
`${facts.unreadable.join(", ")}.`
|
|
157
158
|
);
|
|
158
159
|
}
|
|
159
160
|
for (const entry of facts.unreachable) {
|