@sous-io/sous 0.2.15 → 0.2.17

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.
Files changed (39) hide show
  1. package/docs/markdown/commands.md +63 -16
  2. package/docs/markdown/repositories-authoring.md +52 -11
  3. package/docs/markdown/repositories-consuming.md +32 -1
  4. package/docs/markdown/repositories-file-formats.md +20 -0
  5. package/docs/markdown/repositories-providers.md +20 -10
  6. package/package.json +1 -1
  7. package/recipes/core/sous-skills/sous.recipe.yaml +8 -1
  8. package/src/commands/namespace/list.ts +42 -21
  9. package/src/commands/namespace/show.ts +32 -12
  10. package/src/commands/recipe/list.ts +40 -23
  11. package/src/commands/recipe/show.ts +28 -6
  12. package/src/commands/repo/list.ts +67 -10
  13. package/src/commands/repo/release.ts +41 -0
  14. package/src/commands/repo/search.ts +68 -15
  15. package/src/commands/repo/submit.ts +245 -35
  16. package/src/commands/subscription/list.ts +98 -19
  17. package/src/lib/build-preparation.ts +44 -1
  18. package/src/lib/repos/catalog-display.ts +101 -2
  19. package/src/lib/repos/catalog-inputs.ts +145 -17
  20. package/src/lib/repos/catalog.ts +92 -5
  21. package/src/lib/repos/formats/common.ts +20 -0
  22. package/src/lib/repos/formats/recipe-manifest.ts +7 -0
  23. package/src/lib/repos/formats/repo-manifest.ts +8 -0
  24. package/src/lib/repos/freshness.ts +56 -0
  25. package/src/lib/repos/providers/base.ts +33 -1
  26. package/src/lib/repos/providers/github.ts +276 -3
  27. package/src/lib/repos/providers/gitlab.ts +1 -0
  28. package/src/lib/repos/providers/http.ts +7 -2
  29. package/src/lib/repos/providers/index-cache.ts +56 -17
  30. package/src/lib/repos/providers/provider.ts +121 -3
  31. package/src/lib/repos/release/changelog.ts +448 -0
  32. package/src/lib/repos/release/git-state.ts +101 -15
  33. package/src/lib/repos/release/index.ts +2 -0
  34. package/src/lib/repos/release/submissions.ts +214 -0
  35. package/src/lib/repos/release/submit-checkout.ts +271 -0
  36. package/src/lib/repos/release/submit-questions.ts +153 -0
  37. package/src/lib/repos/release/submit-service.ts +581 -174
  38. package/src/lib/repos/subscription-service.ts +138 -9
  39. package/src/utils/flags.ts +24 -0
@@ -4,23 +4,28 @@
4
4
  * Shows one namespace in full: the repository publishing it, what that
5
5
  * repository says it is for, how much of it this project subscribes to, and
6
6
  * every recipe in it with its latest version, its pinned version and its own
7
- * subscription state. It reads only the indexes sous already has on disk.
7
+ * subscription state. It reads only the indexes sous already has on disk,
8
+ * unless `--latest` asks it to read upstream (which saves nothing); `--installed`
9
+ * narrows the recipes to the ones the lockfile pins.
8
10
  */
9
11
 
10
12
  import { Args } from "@oclif/core";
11
13
  import { BaseCommand } from "../../base-command.js";
12
14
  import { subscriptionServiceFor } from "../../lib/repos/subscription-service.js";
13
- import { catalogContextFor } from "../../lib/repos/catalog-inputs.js";
14
- import { describeNamespace } from "../../lib/repos/catalog.js";
15
+ import { loadCatalogContext } from "../../lib/repos/catalog-inputs.js";
16
+ import { describeInstalled, describeNamespace } from "../../lib/repos/catalog.js";
15
17
  import {
16
18
  INDENT,
17
- RECIPE_COLUMNS,
18
19
  describeCoverage,
20
+ describeIndexSource,
19
21
  factIf,
22
+ printBrowsingNotes,
20
23
  printFacts,
24
+ recipeColumns,
21
25
  recipeRows,
22
26
  } from "../../lib/repos/catalog-display.js";
23
27
  import { renderTable } from "../../utils/table.js";
28
+ import { browsingFlags } from "../../utils/flags.js";
24
29
  import {
25
30
  blankLine,
26
31
  footer,
@@ -53,15 +58,17 @@ export default class NamespaceShow extends BaseCommand {
53
58
  }),
54
59
  };
55
60
 
56
- static flags = { ...BaseCommand.baseFlags };
61
+ static flags = { ...BaseCommand.baseFlags, ...browsingFlags() };
57
62
 
58
63
  async run(): Promise<void> {
59
- const { args } = await this.parse(NamespaceShow);
64
+ const { args, flags } = await this.parse(NamespaceShow);
60
65
 
61
66
  showCommandVars({
62
67
  Project: this.projectLabel,
63
68
  Config: this.configContext.configPath,
64
69
  Namespace: args.ref,
70
+ Reading: describeIndexSource(flags.latest),
71
+ ...(flags.installed ? { Showing: "only what this project has installed" } : {}),
65
72
  });
66
73
 
67
74
  const service = subscriptionServiceFor({
@@ -70,13 +77,16 @@ export default class NamespaceShow extends BaseCommand {
70
77
  shellEnv: this.shellEnv,
71
78
  });
72
79
 
73
- const { inputs } = catalogContextFor({
80
+ const { inputs, notChecked } = await loadCatalogContext({
74
81
  service,
75
82
  sousDir: this.configContext.sousDir,
76
83
  settings: this.settings,
84
+ latest: flags.latest,
77
85
  });
78
86
 
79
- const detail = describeNamespace(inputs, args.ref);
87
+ const detail = flags.installed
88
+ ? describeInstalled(inputs, args.ref, describeNamespace, "namespace")
89
+ : describeNamespace(inputs, args.ref);
80
90
 
81
91
  heading(`The namespace ${detail.namespace}`);
82
92
  blankLine();
@@ -85,7 +95,10 @@ export default class NamespaceShow extends BaseCommand {
85
95
  { label: "Repository", lines: [detail.repo] },
86
96
  ...factIf("Location", detail.repoUrl),
87
97
  ...factIf("About", detail.description),
88
- { label: "Recipes", lines: [String(detail.recipes.length)] },
98
+ {
99
+ label: flags.installed ? "Installed recipes" : "Recipes",
100
+ lines: [String(detail.recipes.length)],
101
+ },
89
102
  { label: "Subscribed", lines: [describeCoverage(detail.subscribed)] },
90
103
  ]);
91
104
 
@@ -99,12 +112,19 @@ export default class NamespaceShow extends BaseCommand {
99
112
  return;
100
113
  }
101
114
 
102
- for (const line of renderTable(RECIPE_COLUMNS, recipeRows(detail.recipes), {
103
- indent: INDENT,
104
- })) {
115
+ for (const line of renderTable(
116
+ recipeColumns({ installed: flags.installed }),
117
+ recipeRows(detail.recipes),
118
+ { indent: INDENT }
119
+ )) {
105
120
  log(indent(line, INDENT));
106
121
  }
107
122
 
123
+ printBrowsingNotes({
124
+ notChecked: notChecked.filter((name) => name === detail.repo),
125
+ anyLinked: detail.recipes.some((entry) => entry.linkedPath !== undefined),
126
+ });
127
+
108
128
  footer();
109
129
  }
110
130
  }
@@ -3,21 +3,26 @@
3
3
  *
4
4
  * Shows every recipe the repositories this project trusts publish, with the
5
5
  * latest published version, the version this project pins, and whether it is
6
- * subscribed. It reads only the indexes sous already has on disk, so it works
7
- * offline; a repository whose index has never been fetched is named at the end
8
- * rather than being silently left out.
6
+ * subscribed. By default it reads only the indexes sous already has on disk, so
7
+ * it works offline; a repository whose index has never been fetched is named at
8
+ * the end rather than being silently left out. `--latest` reads the indexes
9
+ * from upstream instead, without saving them, and `--installed` narrows the
10
+ * listing to what the lockfile pins.
9
11
  */
10
12
 
11
13
  import { BaseCommand } from "../../base-command.js";
12
14
  import { subscriptionServiceFor } from "../../lib/repos/subscription-service.js";
13
- import { catalogContextFor } from "../../lib/repos/catalog-inputs.js";
14
- import { listRecipes } from "../../lib/repos/catalog.js";
15
+ import { loadCatalogContext } from "../../lib/repos/catalog-inputs.js";
16
+ import { listRecipes, narrowToInstalled } from "../../lib/repos/catalog.js";
15
17
  import {
16
18
  INDENT,
17
- RECIPE_COLUMNS,
19
+ describeIndexSource,
20
+ printBrowsingNotes,
21
+ recipeColumns,
18
22
  recipeRows,
19
23
  } from "../../lib/repos/catalog-display.js";
20
24
  import { renderTable } from "../../utils/table.js";
25
+ import { browsingFlags } from "../../utils/flags.js";
21
26
  import {
22
27
  blankLine,
23
28
  footer,
@@ -37,19 +42,28 @@ export default class RecipeList extends BaseCommand {
37
42
  */
38
43
  static aliases = ["recipes:list"];
39
44
 
40
- static examples = ["<%= config.bin %> recipe list"];
45
+ static examples = [
46
+ "<%= config.bin %> recipe list",
47
+ "<%= config.bin %> recipe list --installed --latest",
48
+ ];
41
49
 
42
- static flags = { ...BaseCommand.baseFlags };
50
+ static flags = { ...BaseCommand.baseFlags, ...browsingFlags() };
43
51
 
44
52
  async run(): Promise<void> {
45
- await this.parse(RecipeList);
53
+ const { flags } = await this.parse(RecipeList);
46
54
 
47
55
  showCommandVars({
48
56
  Project: this.projectLabel,
49
57
  Config: this.configContext.configPath,
58
+ Reading: describeIndexSource(flags.latest),
59
+ ...(flags.installed ? { Showing: "only what this project has installed" } : {}),
50
60
  });
51
61
 
52
- heading("Recipes in the repositories this project trusts");
62
+ heading(
63
+ flags.installed
64
+ ? "Recipes this project has installed"
65
+ : "Recipes in the repositories this project trusts"
66
+ );
53
67
 
54
68
  const service = subscriptionServiceFor({
55
69
  configContext: this.configContext,
@@ -57,13 +71,14 @@ export default class RecipeList extends BaseCommand {
57
71
  shellEnv: this.shellEnv,
58
72
  });
59
73
 
60
- const { inputs, notFetched } = catalogContextFor({
74
+ const { inputs, notFetched, notChecked } = await loadCatalogContext({
61
75
  service,
62
76
  sousDir: this.configContext.sousDir,
63
77
  settings: this.settings,
78
+ latest: flags.latest,
64
79
  });
65
80
 
66
- const listings = listRecipes(inputs);
81
+ const listings = listRecipes(flags.installed ? narrowToInstalled(inputs) : inputs);
67
82
 
68
83
  blankLine();
69
84
 
@@ -72,23 +87,25 @@ export default class RecipeList extends BaseCommand {
72
87
  inputs.repos.length === 0
73
88
  ? "Sous has read no repository index for this project, so there are no " +
74
89
  "recipes to show."
75
- : "The repositories this project trusts publish no recipes."
90
+ : flags.installed
91
+ ? "This project has installed no recipe from the repositories it trusts."
92
+ : "The repositories this project trusts publish no recipes."
76
93
  );
77
94
  } else {
78
- for (const line of renderTable(RECIPE_COLUMNS, recipeRows(listings), {
79
- indent: INDENT,
80
- })) {
95
+ for (const line of renderTable(
96
+ recipeColumns({ installed: flags.installed }),
97
+ recipeRows(listings),
98
+ { indent: INDENT }
99
+ )) {
81
100
  log(indent(line, INDENT));
82
101
  }
83
102
  }
84
103
 
85
- if (notFetched.length > 0) {
86
- blankLine();
87
- paragraph(
88
- `These repositories are trusted and their index has not been fetched yet, so ` +
89
- `nothing in them is listed: ${notFetched.join(", ")}.`
90
- );
91
- }
104
+ printBrowsingNotes({
105
+ notFetched,
106
+ notChecked,
107
+ anyLinked: listings.some((entry) => entry.linkedPath !== undefined),
108
+ });
92
109
 
93
110
  footer();
94
111
  }
@@ -6,14 +6,17 @@
6
6
  * and where its files land in this project. It reads only what sous already has
7
7
  * on disk: the repository's cached index, the project's lockfile, and the
8
8
  * recipe's own files when they are in the store or a linked working copy.
9
+ * `--latest` reads the repository's index from upstream instead, saving
10
+ * nothing, and `--installed` looks the ref up among installed recipes only.
9
11
  */
10
12
 
11
13
  import { Args } from "@oclif/core";
12
14
  import { BaseCommand } from "../../base-command.js";
13
15
  import { resolveRootScope } from "../../lib/settings.js";
14
16
  import { subscriptionServiceFor } from "../../lib/repos/subscription-service.js";
15
- import { catalogContextFor } from "../../lib/repos/catalog-inputs.js";
17
+ import { loadCatalogContext } from "../../lib/repos/catalog-inputs.js";
16
18
  import {
19
+ describeInstalled,
17
20
  describeRecipe,
18
21
  type RecipeContentListing,
19
22
  type RecipeDependencyListing,
@@ -22,11 +25,14 @@ import {
22
25
  } from "../../lib/repos/catalog.js";
23
26
  import {
24
27
  INDENT,
28
+ describeIndexSource,
25
29
  describeVersionStatus,
26
30
  factIf,
31
+ printBrowsingNotes,
27
32
  printFacts,
28
33
  } from "../../lib/repos/catalog-display.js";
29
34
  import { renderTable, type TableColumn } from "../../utils/table.js";
35
+ import { browsingFlags } from "../../utils/flags.js";
30
36
  import {
31
37
  blankLine,
32
38
  footer,
@@ -102,15 +108,17 @@ export default class RecipeShow extends BaseCommand {
102
108
  }),
103
109
  };
104
110
 
105
- static flags = { ...BaseCommand.baseFlags };
111
+ static flags = { ...BaseCommand.baseFlags, ...browsingFlags() };
106
112
 
107
113
  async run(): Promise<void> {
108
- const { args } = await this.parse(RecipeShow);
114
+ const { args, flags } = await this.parse(RecipeShow);
109
115
 
110
116
  showCommandVars({
111
117
  Project: this.projectLabel,
112
118
  Config: this.configContext.configPath,
113
119
  Recipe: args.ref,
120
+ Reading: describeIndexSource(flags.latest),
121
+ ...(flags.installed ? { Showing: "only what this project has installed" } : {}),
114
122
  });
115
123
 
116
124
  const service = subscriptionServiceFor({
@@ -119,14 +127,17 @@ export default class RecipeShow extends BaseCommand {
119
127
  shellEnv: this.shellEnv,
120
128
  });
121
129
 
122
- const { inputs } = catalogContextFor({
130
+ const { inputs, notChecked } = await loadCatalogContext({
123
131
  service,
124
132
  sousDir: this.configContext.sousDir,
125
133
  settings: this.settings,
126
134
  scope: resolveRootScope(this.settings, this.configContext),
135
+ latest: flags.latest,
127
136
  });
128
137
 
129
- const detail = describeRecipe(inputs, args.ref);
138
+ const detail = flags.installed
139
+ ? describeInstalled(inputs, args.ref, describeRecipe, "recipe")
140
+ : describeRecipe(inputs, args.ref);
130
141
 
131
142
  heading(detail.key);
132
143
  blankLine();
@@ -137,11 +148,22 @@ export default class RecipeShow extends BaseCommand {
137
148
  ...factIf("About", detail.description),
138
149
  { label: "Folder", lines: [detail.path] },
139
150
  { label: "Latest version", lines: [detail.latest ?? "none published"] },
140
- { label: "Pinned version", lines: [detail.pinned ?? "this project pins none"] },
151
+ {
152
+ label: flags.installed ? "Installed version" : "Pinned version",
153
+ lines: [detail.pinned ?? "this project pins none"],
154
+ },
155
+ ...factIf(
156
+ "Linked",
157
+ detail.linkedPath === undefined
158
+ ? undefined
159
+ : `builds currently read this recipe from the checkout at ${detail.linkedPath}`
160
+ ),
141
161
  { label: "Subscribed", lines: [detail.subscribed ? "yes" : "no"] },
142
162
  ...factIf("Described below", detail.describing),
143
163
  ]);
144
164
 
165
+ printBrowsingNotes({ notChecked: notChecked.filter((name) => name === detail.repo) });
166
+
145
167
  this.printVersions(detail.versions);
146
168
  this.printDependencies(detail.dependencies, detail.manifestRead);
147
169
 
@@ -3,9 +3,12 @@
3
3
  *
4
4
  * Shows every repository this project trusts, what it publishes, and whether it
5
5
  * is currently being read from a working copy instead of a published version.
6
- * It reads only what sous already has on disk: a repository whose index has
7
- * never been fetched says so in its own row rather than triggering a download,
8
- * so the command is safe to run offline.
6
+ * By default it reads only what sous already has on disk: a repository whose
7
+ * index has never been fetched says so in its own row rather than triggering a
8
+ * download, so the command is safe to run offline. `--latest` reads each index
9
+ * from upstream instead, saving nothing, and `--installed` narrows the listing
10
+ * to the repositories the lockfile pins a recipe from, naming each one under
11
+ * its row.
9
12
  */
10
13
 
11
14
  import { Flags } from "@oclif/core";
@@ -15,14 +18,24 @@ import { subscriptionServiceFor } from "../../lib/repos/subscription-service.js"
15
18
  import { readEffectiveLinks } from "../../lib/repos/links.js";
16
19
  import { BUILT_IN_ADDED_BY } from "../../lib/repos/defaults.js";
17
20
  import { requireProvider } from "../../lib/repos/providers/index.js";
21
+ import { readTrustedIndexes } from "../../lib/repos/catalog-inputs.js";
22
+ import {
23
+ LINKED_REPO_NOTE,
24
+ describeIndexSource,
25
+ printBrowsingNotes,
26
+ } from "../../lib/repos/catalog-display.js";
18
27
  import { renderTable, type TableColumn } from "../../utils/table.js";
28
+ import { browsingFlags } from "../../utils/flags.js";
19
29
  import {
20
30
  footer,
21
31
  indent,
22
32
  log,
33
+ note,
23
34
  paragraph,
24
35
  section,
25
36
  showCommandVars,
37
+ wrapColumns,
38
+ wrapText,
26
39
  } from "../../utils/formatting.js";
27
40
 
28
41
  /** How far every line of this command's output is indented. */
@@ -71,10 +84,12 @@ export default class RepoList extends BaseCommand {
71
84
  static examples = [
72
85
  "<%= config.bin %> repo list",
73
86
  "<%= config.bin %> repo list --verbose",
87
+ "<%= config.bin %> repo list --installed --latest",
74
88
  ];
75
89
 
76
90
  static flags = {
77
91
  ...BaseCommand.baseFlags,
92
+ ...browsingFlags(),
78
93
  verbose: Flags.boolean({
79
94
  description: "Show the namespaces each repository publishes, under its row",
80
95
  default: false,
@@ -87,9 +102,15 @@ export default class RepoList extends BaseCommand {
87
102
  showCommandVars({
88
103
  Project: this.projectLabel,
89
104
  Config: this.configContext.configPath,
105
+ Reading: describeIndexSource(flags.latest),
106
+ ...(flags.installed ? { Showing: "only what this project has installed" } : {}),
90
107
  });
91
108
 
92
- section("Repositories this project trusts");
109
+ section(
110
+ flags.installed
111
+ ? "Repositories this project has installed recipes from"
112
+ : "Repositories this project trusts"
113
+ );
93
114
 
94
115
  const service = subscriptionServiceFor({
95
116
  configContext: this.configContext,
@@ -98,7 +119,20 @@ export default class RepoList extends BaseCommand {
98
119
  });
99
120
 
100
121
  const repos = service.currentRepos();
101
- const names = Object.keys(repos).sort();
122
+ const lock = service.lockService.read();
123
+ const names = Object.keys(repos)
124
+ .sort()
125
+ .filter(
126
+ (name) =>
127
+ !flags.installed ||
128
+ Object.values(lock.recipes).some((locked) => locked.repo === name)
129
+ );
130
+
131
+ if (names.length === 0 && flags.installed && Object.keys(repos).length > 0) {
132
+ paragraph("This project has installed no recipe from the repositories it trusts.");
133
+ footer();
134
+ return;
135
+ }
102
136
 
103
137
  if (names.length === 0) {
104
138
  paragraph(
@@ -110,10 +144,12 @@ export default class RepoList extends BaseCommand {
110
144
  }
111
145
 
112
146
  const links = readEffectiveLinks(this.configContext.sousDir);
147
+ const indexes = await readTrustedIndexes(service, { latest: flags.latest });
148
+ const indexFor = new Map(indexes.repos.map((entry) => [entry.name, entry.index]));
113
149
 
114
150
  const rows = names.map((name) => {
115
151
  const entry = repos[name]!;
116
- const index = service.cachedIndex(name);
152
+ const index = indexFor.get(name);
117
153
  const namespaces =
118
154
  index === undefined ? "not fetched" : Object.keys(index.namespaces).sort().join(", ");
119
155
  // A repository whose index has never been fetched says so in the cell
@@ -130,19 +166,40 @@ export default class RepoList extends BaseCommand {
130
166
  namespaces: namespaces.length > 0 ? namespaces : "none",
131
167
  recipes,
132
168
  linked: links[name] === undefined ? "no" : `yes: ${links[name]!.path}`,
169
+ installed: Object.entries(lock.recipes)
170
+ .filter(([, locked]) => locked.repo === name)
171
+ .map(([key, locked]) => `${key} ${locked.version}`)
172
+ .sort()
173
+ .join(", "),
133
174
  };
134
175
  });
135
176
 
136
177
  for (const line of renderTable(COLUMNS, rows, {
137
178
  indent: INDENT,
138
- rowNote: flags.verbose
139
- ? (row) => color.gray(indent(`Namespaces: ${row.namespaces}`, INDENT))
140
- : undefined,
179
+ rowNote: (row) => {
180
+ const notes: string[] = [];
181
+ if (flags.verbose) notes.push(`Namespaces: ${row.namespaces}`);
182
+ if (flags.installed) notes.push(`Installed: ${row.installed}`);
183
+ // Each note wraps under itself, inside the two indents it is printed at.
184
+ const width = Math.max(20, wrapColumns() - INDENT * 2);
185
+ return notes.length === 0
186
+ ? undefined
187
+ : notes
188
+ .flatMap((text) => wrapText(text, width))
189
+ .map((line) => color.gray(indent(line, INDENT)))
190
+ .join("\n");
191
+ },
141
192
  })) {
142
193
  log(indent(line, INDENT));
143
194
  }
144
195
 
145
- // Everything the table can say, the table says; nothing goes under it.
196
+ // The table says everything about each repository; what sits under it is
197
+ // only what the rows could not: a repository upstream could not answer for,
198
+ // and what a linked row means for the installed recipes it names.
199
+ printBrowsingNotes({ notChecked: indexes.notChecked.filter((name) => names.includes(name)) });
200
+ if (flags.installed && names.some((name) => links[name] !== undefined)) {
201
+ note(LINKED_REPO_NOTE);
202
+ }
146
203
  footer();
147
204
  }
148
205
  }
@@ -11,6 +11,7 @@ import {
11
11
  anythingToCommit,
12
12
  buildIndex,
13
13
  buildReleasePlan,
14
+ checkSubmissions,
14
15
  bumpRecipeVersion,
15
16
  commitPaths,
16
17
  createAnnotatedTag,
@@ -391,6 +392,46 @@ export default class RepoRelease extends Command {
391
392
  log(" records for every version it publishes.");
392
393
  }
393
394
  reportPending(result, "These versions have no tag yet; they publish when this merges:");
395
+
396
+ // A pull request can be opened without `sous repo submit`, so this check is
397
+ // the one gate every change passes. The release itself is not restricted:
398
+ // whatever publishes such a recipe still releases it.
399
+ section("Checking the recipes that take no proposals");
400
+ const submissions = await checkSubmissions(validation);
401
+ if (submissions.refusing.length > 0) {
402
+ const problems: ValidationProblem[] = submissions.refusing.map((recipe) => ({
403
+ level: "error",
404
+ where: recipe.path,
405
+ message:
406
+ `this change touches '${recipe.key}', which does not take proposed changes ` +
407
+ `(${recipe.declaredBy === "recipe" ? "its own manifest" : "the repository manifest"} ` +
408
+ `says so). Merging it would break whatever publishes the recipe.` +
409
+ (recipe.instead === undefined ? "" : ` Instead: ${recipe.instead}`),
410
+ }));
411
+ reportProblems(problems);
412
+ displayErrorBlock(
413
+ `This change cannot be merged: it touches ` +
414
+ `${describeCount(problems.length, "recipe")} that ${problems.length === 1 ? "does" : "do"} ` +
415
+ `not take proposed changes, listed above.\n` +
416
+ ` Take those edits out of the change, and send them where each recipe asks.`
417
+ );
418
+ return this.exit(1);
419
+ }
420
+ if (submissions.comparedWith.kind === "branch") {
421
+ log(
422
+ ` Compared with the branch '${submissions.comparedWith.branch}': this change touches no ` +
423
+ `recipe that declines proposals.`
424
+ );
425
+ } else if (submissions.comparedWith.kind === "tags") {
426
+ log(" Compared with each recipe's last release tag: nothing that declines proposals changed.");
427
+ } else if (submissions.comparedWith.kind === "none declined") {
428
+ log(" No recipe in this repository declines proposed changes.");
429
+ } else {
430
+ note(
431
+ "This checkout holds neither a copy of the default branch nor a release tag of any " +
432
+ "recipe that declines proposed changes, so there was nothing to compare with."
433
+ );
434
+ }
394
435
  footer();
395
436
  }
396
437