@sous-io/sous 0.2.37 → 1.0.1

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 (244) hide show
  1. package/README.md +14 -256
  2. package/dist/bin.js +11 -0
  3. package/dist/sous.js +1093 -0
  4. package/docs/markdown/README.md +5 -4
  5. package/docs/markdown/_sidebar.md +21 -1
  6. package/docs/markdown/commands.md +294 -431
  7. package/docs/markdown/config-discovery.md +25 -14
  8. package/docs/markdown/config-inspection.md +53 -31
  9. package/docs/markdown/config-layers.md +168 -66
  10. package/docs/markdown/config-variables.md +44 -11
  11. package/docs/markdown/configuration.md +104 -17
  12. package/docs/markdown/design-principles.md +4 -1
  13. package/docs/markdown/repositories-authoring.md +30 -0
  14. package/docs/markdown/repositories-consuming.md +72 -14
  15. package/docs/markdown/repositories-file-formats.md +31 -13
  16. package/docs/markdown/repositories-providers.md +48 -57
  17. package/docs/markdown/repositories-quickstart.md +3 -2
  18. package/docs/markdown/repositories-troubleshooting.md +27 -1
  19. package/docs/markdown/repositories-variables.md +6 -1
  20. package/docs/markdown/repositories.md +18 -8
  21. package/package.json +48 -107
  22. package/recipes/core/sous-skills/skills/about-liquid-templates/SKILL.tpl.md +4 -6
  23. package/recipes/core/sous-skills/skills/about-liquid-templates/references/liquid-filters.tpl.md +2 -0
  24. package/recipes/core/sous-skills/skills/about-sous/SKILL.tpl.md +10 -9
  25. package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +35 -14
  26. package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +51 -20
  27. package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
  28. package/sous.config.schema.json +351 -132
  29. package/bin/run.js +0 -51
  30. package/src/base-command.ts +0 -457
  31. package/src/commands/build.ts +0 -181
  32. package/src/commands/clear.ts +0 -79
  33. package/src/commands/compile.ts +0 -155
  34. package/src/commands/config/get.ts +0 -110
  35. package/src/commands/config/show.ts +0 -32
  36. package/src/commands/config/validate.ts +0 -53
  37. package/src/commands/help.ts +0 -46
  38. package/src/commands/init.ts +0 -289
  39. package/src/commands/launch.ts +0 -139
  40. package/src/commands/lock/rebuild.ts +0 -239
  41. package/src/commands/lock/show.ts +0 -115
  42. package/src/commands/namespace/list.ts +0 -138
  43. package/src/commands/namespace/show.ts +0 -130
  44. package/src/commands/prune.ts +0 -43
  45. package/src/commands/recipe/list.ts +0 -112
  46. package/src/commands/recipe/show.ts +0 -439
  47. package/src/commands/repo/add.ts +0 -145
  48. package/src/commands/repo/contribute.ts +0 -604
  49. package/src/commands/repo/gc.ts +0 -173
  50. package/src/commands/repo/init.ts +0 -136
  51. package/src/commands/repo/link.ts +0 -899
  52. package/src/commands/repo/list.ts +0 -236
  53. package/src/commands/repo/release.ts +0 -685
  54. package/src/commands/repo/remove.ts +0 -194
  55. package/src/commands/repo/search.ts +0 -243
  56. package/src/commands/repo/submit.ts +0 -341
  57. package/src/commands/repo/unlink.ts +0 -461
  58. package/src/commands/subscription/add.ts +0 -286
  59. package/src/commands/subscription/list.ts +0 -208
  60. package/src/commands/subscription/remove.ts +0 -182
  61. package/src/commands/subscription/update.ts +0 -216
  62. package/src/commands/update.ts +0 -178
  63. package/src/commands/vars/ask.ts +0 -417
  64. package/src/commands/vars/index.ts +0 -79
  65. package/src/commands/vars/list.ts +0 -67
  66. package/src/commands/vars/show.ts +0 -77
  67. package/src/config-command.ts +0 -30
  68. package/src/lib/build-preparation.ts +0 -128
  69. package/src/lib/build-service.ts +0 -580
  70. package/src/lib/config-discovery.ts +0 -378
  71. package/src/lib/config-inspect.ts +0 -145
  72. package/src/lib/config-kernel.mjs +0 -379
  73. package/src/lib/config-schema.ts +0 -401
  74. package/src/lib/env-file.ts +0 -328
  75. package/src/lib/env-local.ts +0 -212
  76. package/src/lib/errors.ts +0 -32
  77. package/src/lib/glob-pattern.ts +0 -24
  78. package/src/lib/include-resolver.ts +0 -523
  79. package/src/lib/interactive.ts +0 -165
  80. package/src/lib/markdown-compiler.ts +0 -947
  81. package/src/lib/package-info.ts +0 -25
  82. package/src/lib/pid-service.ts +0 -99
  83. package/src/lib/project-install.d.mts +0 -44
  84. package/src/lib/project-install.mjs +0 -234
  85. package/src/lib/project-scaffold/index.ts +0 -332
  86. package/src/lib/project-scaffold/templates.ts +0 -240
  87. package/src/lib/repos/catalog-display.ts +0 -215
  88. package/src/lib/repos/catalog-inputs.ts +0 -288
  89. package/src/lib/repos/catalog.ts +0 -842
  90. package/src/lib/repos/contribute.ts +0 -452
  91. package/src/lib/repos/core-recipe.ts +0 -105
  92. package/src/lib/repos/declarations.ts +0 -187
  93. package/src/lib/repos/defaults.ts +0 -175
  94. package/src/lib/repos/formats/common.ts +0 -440
  95. package/src/lib/repos/formats/index-file.ts +0 -253
  96. package/src/lib/repos/formats/links-map.ts +0 -98
  97. package/src/lib/repos/formats/lockfile.ts +0 -169
  98. package/src/lib/repos/formats/patterns.ts +0 -57
  99. package/src/lib/repos/formats/recipe-manifest.ts +0 -205
  100. package/src/lib/repos/formats/repo-manifest.ts +0 -97
  101. package/src/lib/repos/formats/store-entry.ts +0 -84
  102. package/src/lib/repos/formats/variable-definition.ts +0 -268
  103. package/src/lib/repos/freshness.ts +0 -264
  104. package/src/lib/repos/git-clone.ts +0 -850
  105. package/src/lib/repos/identity.ts +0 -89
  106. package/src/lib/repos/index.ts +0 -57
  107. package/src/lib/repos/links.ts +0 -356
  108. package/src/lib/repos/load-manifest.ts +0 -237
  109. package/src/lib/repos/lock-service.ts +0 -454
  110. package/src/lib/repos/locked-namespace-resolver.ts +0 -232
  111. package/src/lib/repos/locked-recipe-lookup.ts +0 -123
  112. package/src/lib/repos/locked-recipes.ts +0 -254
  113. package/src/lib/repos/managed-layer.ts +0 -422
  114. package/src/lib/repos/memories-hash-name.ts +0 -33
  115. package/src/lib/repos/namespace-resolver.ts +0 -527
  116. package/src/lib/repos/providers/base.ts +0 -268
  117. package/src/lib/repos/providers/git.ts +0 -245
  118. package/src/lib/repos/providers/github.ts +0 -589
  119. package/src/lib/repos/providers/gitlab.ts +0 -332
  120. package/src/lib/repos/providers/http.ts +0 -128
  121. package/src/lib/repos/providers/index-cache.ts +0 -421
  122. package/src/lib/repos/providers/index.ts +0 -106
  123. package/src/lib/repos/providers/local.ts +0 -410
  124. package/src/lib/repos/providers/provider.ts +0 -615
  125. package/src/lib/repos/recipe-config-layers.ts +0 -316
  126. package/src/lib/repos/recipe-key-matcher.ts +0 -62
  127. package/src/lib/repos/recipe-memories.ts +0 -178
  128. package/src/lib/repos/recipe-targets.ts +0 -236
  129. package/src/lib/repos/ref-search.ts +0 -46
  130. package/src/lib/repos/release/bump.ts +0 -161
  131. package/src/lib/repos/release/changelog.ts +0 -461
  132. package/src/lib/repos/release/git-state.ts +0 -460
  133. package/src/lib/repos/release/index-builder.ts +0 -1108
  134. package/src/lib/repos/release/index.ts +0 -19
  135. package/src/lib/repos/release/plan.ts +0 -521
  136. package/src/lib/repos/release/settle.ts +0 -154
  137. package/src/lib/repos/release/submissions.ts +0 -214
  138. package/src/lib/repos/release/submit-checkout.ts +0 -290
  139. package/src/lib/repos/release/submit-questions.ts +0 -153
  140. package/src/lib/repos/release/submit-service.ts +0 -898
  141. package/src/lib/repos/release/tags.ts +0 -239
  142. package/src/lib/repos/release/validate.ts +0 -463
  143. package/src/lib/repos/resolver.ts +0 -1028
  144. package/src/lib/repos/scaffold/index.ts +0 -245
  145. package/src/lib/repos/scaffold/templates.ts +0 -517
  146. package/src/lib/repos/seed.ts +0 -473
  147. package/src/lib/repos/store/contract.ts +0 -64
  148. package/src/lib/repos/store/hash.ts +0 -191
  149. package/src/lib/repos/store/recipe-store.ts +0 -599
  150. package/src/lib/repos/store/settings.ts +0 -58
  151. package/src/lib/repos/subscription-service.ts +0 -3859
  152. package/src/lib/repos/trust.ts +0 -447
  153. package/src/lib/repos/update-plan.ts +0 -235
  154. package/src/lib/self-update/index.ts +0 -14
  155. package/src/lib/self-update/installs.ts +0 -413
  156. package/src/lib/self-update/managers.ts +0 -126
  157. package/src/lib/self-update/plan.ts +0 -315
  158. package/src/lib/self-update/registry.ts +0 -99
  159. package/src/lib/self-update/run.ts +0 -506
  160. package/src/lib/self-update/versions.ts +0 -243
  161. package/src/lib/settings.ts +0 -1107
  162. package/src/lib/sous-home.ts +0 -104
  163. package/src/lib/state.ts +0 -167
  164. package/src/lib/vars/answers.ts +0 -192
  165. package/src/lib/vars/ask.ts +0 -1152
  166. package/src/lib/vars/definition-source.ts +0 -261
  167. package/src/lib/vars/display.ts +0 -231
  168. package/src/lib/vars/index.ts +0 -20
  169. package/src/lib/vars/ladder.ts +0 -282
  170. package/src/lib/vars/mappings.ts +0 -260
  171. package/src/lib/vars/names.ts +0 -94
  172. package/src/lib/vars/preanswers.ts +0 -395
  173. package/src/lib/vars/question-plan.ts +0 -241
  174. package/src/lib/vars/report.ts +0 -228
  175. package/src/lib/vars/safe-regex.ts +0 -235
  176. package/src/lib/vars/secrets.ts +0 -131
  177. package/src/lib/vars/validate.ts +0 -312
  178. package/src/lib/version-report.ts +0 -90
  179. package/src/lib/watch-loop.ts +0 -148
  180. package/src/lib/watch-service.ts +0 -116
  181. package/src/services/ref-resolver/container.ts +0 -107
  182. package/src/services/ref-resolver/dependency.ts +0 -60
  183. package/src/services/ref-resolver/format.ts +0 -197
  184. package/src/services/ref-resolver/glob.ts +0 -115
  185. package/src/services/ref-resolver/hash-names.ts +0 -155
  186. package/src/services/ref-resolver/index.ts +0 -55
  187. package/src/services/ref-resolver/injectable.ts +0 -36
  188. package/src/services/ref-resolver/location.ts +0 -62
  189. package/src/services/ref-resolver/lookups/cached-index-lookup.ts +0 -53
  190. package/src/services/ref-resolver/lookups/catalog-lookup.ts +0 -23
  191. package/src/services/ref-resolver/lookups/catalog-matcher.ts +0 -261
  192. package/src/services/ref-resolver/lookups/chained-lookup.ts +0 -27
  193. package/src/services/ref-resolver/lookups/env-var-lookup.ts +0 -65
  194. package/src/services/ref-resolver/lookups/fetched-index-lookup.ts +0 -150
  195. package/src/services/ref-resolver/lookups/recorded-dependency-lookup.ts +0 -62
  196. package/src/services/ref-resolver/lookups/ref-lookup.ts +0 -54
  197. package/src/services/ref-resolver/lookups/variable-lookup.ts +0 -123
  198. package/src/services/ref-resolver/named-refs.ts +0 -144
  199. package/src/services/ref-resolver/parser/env-var-splitter.ts +0 -27
  200. package/src/services/ref-resolver/parser/location-splitter.ts +0 -239
  201. package/src/services/ref-resolver/parser/name-path-splitter.ts +0 -200
  202. package/src/services/ref-resolver/parser/partial-ref.ts +0 -84
  203. package/src/services/ref-resolver/parser/query-splitter.ts +0 -61
  204. package/src/services/ref-resolver/parser/range-splitter.ts +0 -52
  205. package/src/services/ref-resolver/parser/ref-parser.ts +0 -115
  206. package/src/services/ref-resolver/parser/repo-qualifier-splitter.ts +0 -51
  207. package/src/services/ref-resolver/parts.ts +0 -115
  208. package/src/services/ref-resolver/pruners/command-line-pruner.ts +0 -47
  209. package/src/services/ref-resolver/pruners/include-pruner.ts +0 -66
  210. package/src/services/ref-resolver/pruners/manifest-pruner.ts +0 -130
  211. package/src/services/ref-resolver/pruners/ref-pruner.ts +0 -135
  212. package/src/services/ref-resolver/pruners/rule-helpers.ts +0 -44
  213. package/src/services/ref-resolver/pruners/stored-key-pruner.ts +0 -136
  214. package/src/services/ref-resolver/ref-helpers.ts +0 -62
  215. package/src/services/ref-resolver/ref-picker-service.ts +0 -204
  216. package/src/services/ref-resolver/ref-report.ts +0 -84
  217. package/src/services/ref-resolver/ref-resolve-arguments.ts +0 -47
  218. package/src/services/ref-resolver/ref-resolve-result.ts +0 -36
  219. package/src/services/ref-resolver/ref-resolver-service.ts +0 -323
  220. package/src/services/ref-resolver/source.ts +0 -48
  221. package/src/services/ref-resolver/tokens.ts +0 -25
  222. package/src/services/ref-resolver/types.ts +0 -155
  223. package/src/templating/filters/bullet-list.ts +0 -9
  224. package/src/templating/filters/index.ts +0 -8
  225. package/src/templating/init-liquid-engine.ts +0 -139
  226. package/src/templating/lib/glob-files.ts +0 -74
  227. package/src/templating/lib/import-export.ts +0 -32
  228. package/src/templating/lib/secret-scope.ts +0 -41
  229. package/src/templating/lib/tag-args.ts +0 -19
  230. package/src/templating/tags/exportScalarVarsJs.ts +0 -46
  231. package/src/templating/tags/getFiles.ts +0 -89
  232. package/src/templating/tags/index.ts +0 -14
  233. package/src/templating/tags/listFiles.ts +0 -54
  234. package/src/templating/tags/showVars.ts +0 -28
  235. package/src/utils/choice-prompt.ts +0 -143
  236. package/src/utils/command-errors.ts +0 -186
  237. package/src/utils/command-help.ts +0 -45
  238. package/src/utils/confirm-prompt.ts +0 -110
  239. package/src/utils/flags.ts +0 -177
  240. package/src/utils/formatting.ts +0 -849
  241. package/src/utils/prompts.ts +0 -53
  242. package/src/utils/sous-directory.ts +0 -245
  243. package/src/utils/table.ts +0 -603
  244. package/src/utils/value-prompt.ts +0 -119
@@ -1,898 +0,0 @@
1
- /**
2
- * Proposing a change to a recipe repository, over the proposal's whole life.
3
- *
4
- * `submit` universally means "propose a change for maintainers to review". It
5
- * never publishes and never writes to a repository directly; the fork and
6
- * proposal mechanics belong to the provider, which knows its own host and
7
- * already has the contributor's credentials through that host's command line
8
- * tool.
9
- *
10
- * One command covers a proposal from start to finish, for the branch that is
11
- * checked out or the one `--branch` names:
12
- *
13
- * - no proposal yet: open one;
14
- * - an open proposal: push whatever is new, which updates it, and replace its
15
- * title or body when new ones are given; with nothing new, report on it;
16
- * - a merged proposal: say so, and continue on a new branch (named, generated,
17
- * or not at all), which gets a new proposal;
18
- * - a proposal closed without merging: say so, and open a fresh one;
19
- * - a pushed branch holding commits the local one lacks: git refuses the push,
20
- * and its refusal is passed through. Nothing here ever forces a push.
21
- *
22
- * This module is a SEQUENCER and nothing more. It knows the order the steps go
23
- * in, what each one is called, and what to say when one fails; it does not know
24
- * that GitHub exists, which tool proposes a change, or how a fork is spelled.
25
- * Every host-specific fact is asked of the provider interface and comes back as
26
- * plain data, which is what keeps a third provider a single new file. Every
27
- * question is asked through `SubmitQuestions`, so the command decides how a
28
- * question is put (or which flag answered it) and this module never prompts.
29
- *
30
- * Three rules shape everything here:
31
- *
32
- * - Everything is worked out, and every question asked, before anything is
33
- * written. A contributor who declines, or a run that cannot ask, leaves the
34
- * checkout exactly as it found it.
35
- * - Nothing is sent until the repository validates and the contributor has left
36
- * its index alone. Whether the index agrees with the tags is the maintainer's
37
- * check (`sous repo release --check`, on a full clone), not the contributor's.
38
- * - Every step announces itself BEFORE it runs, and a failure says exactly which
39
- * steps completed. A half-finished submission (a branch pushed, no proposal
40
- * opened) is a normal outcome of a network failure, and the contributor has to
41
- * be told the truth about it.
42
- *
43
- * Every subprocess goes through the injectable runner, so no test here reaches
44
- * a network.
45
- */
46
-
47
- import { ConfigError } from "../../errors.js";
48
- import { runGit, type CommandRunner } from "../providers/git.js";
49
- import { detectProvider } from "../providers/index.js";
50
- import {
51
- supportsProposals,
52
- supportsSubmit,
53
- type CanonicalRepo,
54
- type ProposalStatus,
55
- type ProposalSummary,
56
- type ProviderOptions,
57
- type RepoProvider,
58
- type SubmitCapableProvider,
59
- } from "../providers/provider.js";
60
- import { INDEX_FILENAME } from "../formats/common.js";
61
- import {
62
- buildChangelog,
63
- composeProposalBody,
64
- readManifestsAt,
65
- renderChangelog,
66
- snapshotOf,
67
- type Changelog,
68
- } from "./changelog.js";
69
- import {
70
- branchExists,
71
- commitEverything,
72
- createBranch,
73
- currentBranch,
74
- defaultBranch,
75
- forkPoint,
76
- hasCommitIdentity,
77
- pathChangedSince,
78
- pathsChangedSince,
79
- pushBranch,
80
- remoteUrl,
81
- submitBranchName,
82
- switchBranch,
83
- uncommittedChanges,
84
- type ChangedPath,
85
- } from "./git-state.js";
86
- import { recipesRefusingSubmissions, type RefusingRecipe } from "./submissions.js";
87
- import {
88
- errorsIn,
89
- hasErrors,
90
- validateRepo,
91
- type RepoValidation,
92
- type ValidationProblem,
93
- } from "./validate.js";
94
-
95
- /** The remote a repository is contributed back to. */
96
- const UPSTREAM_REMOTE = "origin";
97
-
98
- /** The remote name sous gives a fork it created. */
99
- export const FORK_REMOTE = "fork";
100
-
101
- /** What a proposal is called when the provider does not name it. */
102
- const DEFAULT_PROPOSAL_NOUN = "proposal";
103
-
104
- /** How the contributor wants to go on after a proposal was merged. */
105
- export type NextBranchChoice =
106
- | { kind: "name"; name: string }
107
- | { kind: "generate" }
108
- | { kind: "cancel" };
109
-
110
- /**
111
- * Every question a submission may need answered. The command supplies these:
112
- * it knows whether a terminal is attached and which flags were passed, and a
113
- * question that cannot be asked raises the error naming the flag that answers
114
- * it. The sequencer only ever asks, and only before anything is written.
115
- */
116
- export type SubmitQuestions = {
117
- /** The proposal's title, when it is required and was not given. */
118
- title(): Promise<string>;
119
- /** The proposal's description, when it is required and was not given. */
120
- body(): Promise<string>;
121
- /** Whether to commit the listed paths, for `--commit`. */
122
- confirmCommit(paths: ReadonlyArray<ChangedPath>): Promise<boolean>;
123
- /** Whether to propose a change to recipes that say they take no proposals. */
124
- proceedDespiteSubmissions(refusing: ReadonlyArray<RefusingRecipe>): Promise<boolean>;
125
- /** Where to go on after the branch's proposal was merged. */
126
- nextBranch(merged: ProposalSummary, generated: string): Promise<NextBranchChoice>;
127
- };
128
-
129
- /** What `submitRepo` needs to know. */
130
- export type SubmitOptions = {
131
- /** The repository's root directory. */
132
- rootDir: string;
133
- /** The title for the proposal. Required for a new one; replaces an open one's when given. */
134
- title?: string;
135
- /** The description for the proposal. Required for a new one; replaces an open one's when given. */
136
- body?: string;
137
- /** Whether to open a new proposal as a draft. */
138
- draft?: boolean;
139
- /** The branch to work with, instead of the one that is checked out. */
140
- branch?: string;
141
- /** When true, only report on the branch's proposal; nothing is checked, written or sent. */
142
- statusOnly?: boolean;
143
- /** When true, uncommitted changes are committed (after a confirmation) rather than refused. */
144
- commit?: boolean;
145
- /** When true, everything is checked and reported and nothing is written or sent. */
146
- dryRun?: boolean;
147
- /** How questions are asked. Defaults to refusing every one of them. */
148
- questions?: SubmitQuestions;
149
- /** When the submission is happening; decides a generated branch name. Defaults to now. */
150
- now?: Date;
151
- /** How subprocesses are run. Defaults to spawning a real process. */
152
- run?: CommandRunner;
153
- /** The providers to consider. Defaults to the built-in list. */
154
- providers?: RepoProvider[];
155
- /** Called with each step, BEFORE it runs. */
156
- onStep?: (message: string) => void;
157
- /** Called with anything worth saying that is not a step. */
158
- onNotice?: (message: string) => void;
159
- /** Called with a warning the contributor should weigh. Defaults to `onNotice`. */
160
- onWarning?: (message: string) => void;
161
- };
162
-
163
- /**
164
- * How a submission ended.
165
- *
166
- * - `created`: a new proposal was opened (or, on a dry run, would be).
167
- * - `updated`: an open proposal received new commits, a new title or a new body.
168
- * - `unchanged`: an open proposal had nothing new to receive; its status is reported.
169
- * - `status`: `--status` reported on the branch's proposal, or on its absence.
170
- * - `cancelled`: the contributor declined a question, and nothing was written.
171
- */
172
- export type SubmitOutcome = "created" | "updated" | "unchanged" | "status" | "cancelled";
173
-
174
- /** What a submission did. */
175
- export type SubmitResult = {
176
- /** How it ended. */
177
- outcome: SubmitOutcome;
178
- /** The provider the proposal went to. */
179
- provider: string;
180
- /** What the provider calls a proposal. */
181
- proposalNoun: string;
182
- /** The repository, as the provider understands it. */
183
- repo: CanonicalRepo;
184
- /** The branch the change is on. */
185
- branch: string;
186
- /** The branch the proposal targets. */
187
- baseBranch: string;
188
- /** True when the change goes through a fork rather than to the repository itself. */
189
- usedFork: boolean;
190
- /** The remote the branch was (or would be) pushed to. */
191
- pushedTo: string;
192
- /** The proposal's title, when this run set one. */
193
- title?: string;
194
- /** The proposal's URL, when the provider reported one. */
195
- url?: string;
196
- /** The branch's proposal as it stood before this run, when it had one. */
197
- previous?: ProposalSummary;
198
- /** Where the proposal stands, when this run reported on it. */
199
- status?: ProposalStatus;
200
- /** The paths committed for the contributor, with `--commit`. */
201
- committed?: string[];
202
- /** The changelog, when this run generated one. */
203
- changelog?: Changelog;
204
- /** Recipes the change touches that do not take proposals. */
205
- refusing: RefusingRecipe[];
206
- /** Every step that completed, in order. */
207
- completed: string[];
208
- /** True when nothing was actually written or sent. */
209
- dryRun: boolean;
210
- };
211
-
212
- /** The questions a caller that supplied none gets: each one refuses. */
213
- const REFUSING_QUESTIONS: SubmitQuestions = {
214
- title: async () => {
215
- throw new ConfigError("A new proposal needs a title; pass '--title'.");
216
- },
217
- body: async () => {
218
- throw new ConfigError("A new proposal needs a description; pass '--body'.");
219
- },
220
- confirmCommit: async () => false,
221
- proceedDespiteSubmissions: async () => false,
222
- nextBranch: async () => ({ kind: "cancel" }),
223
- };
224
-
225
- /**
226
- * Validates a repository and carries its branch's proposal one step further:
227
- * opens it, updates it, reports on it, or starts the next one.
228
- *
229
- * @param options - The repository, the proposal's text, the flags and the testing seams.
230
- */
231
- export async function submitRepo(options: SubmitOptions): Promise<SubmitResult> {
232
- const { rootDir, draft = false, dryRun = false, now = new Date(), run } = options;
233
- const questions = options.questions ?? REFUSING_QUESTIONS;
234
- const step = options.onStep ?? (() => {});
235
- const notice = options.onNotice ?? (() => {});
236
- const warn = options.onWarning ?? notice;
237
- const completed: string[] = [];
238
-
239
- /** Runs one step, announcing it first and recording it once it succeeds. */
240
- const doStep = async <T>(message: string, action: () => Promise<T>): Promise<T> => {
241
- step(message);
242
- const result = await action().catch((error: unknown) => {
243
- throw partialStateError(message, completed, error);
244
- });
245
- completed.push(message);
246
- return result;
247
- };
248
-
249
- /** Runs one read-only check, announcing it first. */
250
- const check = async <T>(message: string, done: string, action: () => Promise<T>) => {
251
- step(message);
252
- const result = await action();
253
- completed.push(done);
254
- return result;
255
- };
256
-
257
- // --- Preflight: is this a repository sous can propose a change to? --------
258
- //
259
- // The cheap, actionable checks come first. The manifests are read this early
260
- // only for the contribution pointer; what the recipes say is only judged once
261
- // the ground is firm.
262
-
263
- const validation = await check(
264
- "Reading the repository manifest and every recipe in it",
265
- "Read the repository manifest and every recipe in it",
266
- async () => validateRepo(rootDir)
267
- );
268
-
269
- const upstreamUrl = await check(
270
- "Looking up where this repository was cloned from",
271
- "Looked up where this repository was cloned from",
272
- () => remoteUrl(rootDir, UPSTREAM_REMOTE, { run })
273
- );
274
- if (upstreamUrl === undefined) {
275
- throw new ConfigError(
276
- `This repository has no '${UPSTREAM_REMOTE}' remote, so sous cannot tell where to ` +
277
- `propose the change.\n` +
278
- ` Add one with 'git remote add ${UPSTREAM_REMOTE} <url>', then run the command again.`
279
- );
280
- }
281
-
282
- const provider = requireSubmitProvider(upstreamUrl, validation, options.providers);
283
- const repo = provider.canonicalize(upstreamUrl);
284
- const proposalNoun = provider.proposalNoun ?? DEFAULT_PROPOSAL_NOUN;
285
- const tracksProposals = supportsProposals(provider);
286
-
287
- // Everything the provider runs, it runs inside the contributor's checkout.
288
- const providerOptions: ProviderOptions = { cwd: rootDir, run };
289
-
290
- const signInLabel = provider.cli?.label ?? "the repository host's command line tool";
291
- const auth = await check(
292
- `Checking that ${signInLabel} is installed and signed in`,
293
- `Checked that ${signInLabel} is installed and signed in`,
294
- () => provider.authStatus(providerOptions)
295
- );
296
- if (!auth.ok) {
297
- throw new ConfigError(`${auth.detail}\n` + contributePointer(validation));
298
- }
299
-
300
- const baseBranch = (await defaultBranch(rootDir, { run })) ?? "main";
301
- const checkedOut = await currentBranch(rootDir, { run });
302
-
303
- // --- Where the change goes: the repository itself, or a fork -------------
304
-
305
- const canPush = await check(
306
- "Checking whether you can push to the repository itself",
307
- "Checked whether you can push to the repository itself",
308
- () => provider.canPush(repo, providerOptions)
309
- );
310
- const usedFork = canPush === false;
311
- const pushRemote = usedFork ? FORK_REMOTE : UPSTREAM_REMOTE;
312
- if (canPush === undefined) {
313
- notice(
314
- `Sous could not tell whether you can push to ${repo.owner}/${repo.name}, so the change ` +
315
- `goes to '${UPSTREAM_REMOTE}' as it stands.`
316
- );
317
- }
318
- const knownForkOwner = usedFork ? await forkOwnerFromRemote(rootDir, provider, run) : undefined;
319
-
320
- const base = {
321
- provider: provider.id,
322
- proposalNoun,
323
- repo,
324
- baseBranch,
325
- usedFork,
326
- pushedTo: pushRemote,
327
- completed,
328
- dryRun,
329
- };
330
-
331
- /** Looks up the proposal a branch was pushed for, when the provider can. */
332
- const lookUp = async (branch: string): Promise<ProposalSummary | undefined> => {
333
- if (!supportsProposals(provider)) return undefined;
334
- return check(
335
- `Looking for a ${proposalNoun} for the branch '${branch}'`,
336
- `Looked for a ${proposalNoun} for the branch '${branch}'`,
337
- () =>
338
- provider.findProposal(
339
- repo,
340
- {
341
- branch,
342
- fromFork: usedFork,
343
- ...(knownForkOwner === undefined ? {} : { forkOwner: knownForkOwner }),
344
- },
345
- providerOptions
346
- )
347
- );
348
- };
349
-
350
- /** Asks the provider where a proposal stands. */
351
- const statusOf = async (proposal: ProposalSummary): Promise<ProposalStatus> => {
352
- if (!supportsProposals(provider)) return { proposal };
353
- return check(
354
- `Reading where the ${proposalNoun} stands`,
355
- `Read where the ${proposalNoun} stands`,
356
- () => provider.proposalStatus(repo, proposal.id, providerOptions)
357
- );
358
- };
359
-
360
- // --- Status only ------------------------------------------------------------
361
-
362
- if (options.statusOnly === true) {
363
- if (!tracksProposals) {
364
- throw new ConfigError(
365
- `The '${provider.id}' provider cannot look a ${proposalNoun} up after it was opened, ` +
366
- `so sous cannot report on one.\n` +
367
- ` Open the repository on its host to see where the ${proposalNoun} stands.`
368
- );
369
- }
370
- const branch = options.branch ?? checkedOut;
371
- if (branch === undefined) {
372
- throw new ConfigError(
373
- "No branch is checked out, so there is no proposal to report on.\n" +
374
- " Name the branch with '--branch <name>'."
375
- );
376
- }
377
- const found = await lookUp(branch);
378
- const status = found === undefined ? undefined : await statusOf(found);
379
- return {
380
- ...base,
381
- outcome: "status",
382
- branch,
383
- ...(found?.url === undefined ? {} : { url: found.url }),
384
- ...(found === undefined ? {} : { previous: found }),
385
- ...(status === undefined ? {} : { status }),
386
- refusing: [],
387
- dryRun: true,
388
- };
389
- }
390
-
391
- // --- The working tree -------------------------------------------------------
392
-
393
- const uncommitted = await check(
394
- "Checking that everything is committed",
395
- "Checked that everything is committed",
396
- () => uncommittedChanges(rootDir, { run })
397
- );
398
- const toCommit = options.commit === true ? uncommitted : [];
399
-
400
- if (uncommitted.length > 0 && options.commit !== true) {
401
- const listed = uncommitted.map((entry) => ` ${entry.path}`).join("\n");
402
- throw new ConfigError(
403
- "Cannot propose a change while the working tree has uncommitted changes.\n\n" +
404
- `${listed}\n\n` +
405
- " A proposal is made of commits, so everything it should carry has to be " +
406
- "committed first.\n" +
407
- " Commit these yourself, or pass '--commit' to have sous commit them, then run " +
408
- "the command again."
409
- );
410
- }
411
- if (options.commit === true && uncommitted.length === 0) {
412
- notice("Everything is already committed, so '--commit' has nothing to commit.");
413
- }
414
- if (toCommit.some((entry) => entry.path === INDEX_FILENAME)) {
415
- throw indexEditedError(`git checkout HEAD -- ${INDEX_FILENAME}`);
416
- }
417
- if (toCommit.length > 0) {
418
- // Checked before anything is written, so a missing identity is reported
419
- // rather than surfacing as git's own error halfway through.
420
- const identified = await check(
421
- "Checking that git knows who is committing",
422
- "Checked that git knows who is committing",
423
- () => hasCommitIdentity(rootDir, { run })
424
- );
425
- if (!identified) {
426
- throw new ConfigError(
427
- "Sous cannot commit for you, because git cannot work out who is committing.\n" +
428
- " Set it with 'git config user.name \"Your Name\"' and " +
429
- "'git config user.email you@example.com', then run the command again."
430
- );
431
- }
432
- }
433
-
434
- // --- Validate what is about to be proposed --------------------------------
435
-
436
- step("Checking that every recipe describes itself correctly");
437
- assertRepoValidates(validation);
438
- completed.push("Checked that every recipe describes itself correctly");
439
-
440
- const since = await check(
441
- `Checking that ${INDEX_FILENAME} was left alone`,
442
- `Checked that ${INDEX_FILENAME} was left alone`,
443
- async () => {
444
- const point = await forkPoint(rootDir, UPSTREAM_REMOTE, baseBranch, { run });
445
- if (point === undefined) {
446
- notice(
447
- `This checkout holds no copy of '${UPSTREAM_REMOTE}/${baseBranch}', so sous could not ` +
448
- `check whether ${INDEX_FILENAME} was changed, which recipes the change touches, or ` +
449
- `what merging it changes.`
450
- );
451
- } else if (await pathChangedSince(rootDir, point, INDEX_FILENAME, { run })) {
452
- throw indexEditedError(`git checkout ${point.slice(0, 12)} -- ${INDEX_FILENAME}`);
453
- }
454
- return point;
455
- }
456
- );
457
-
458
- // Every path the change touches: its commits, and whatever --commit adds.
459
- const changedPaths = [
460
- ...(since === undefined ? [] : await pathsChangedSince(rootDir, since, { run })),
461
- ...toCommit.map((entry) => entry.path),
462
- ];
463
-
464
- // --- Recipes that take no proposals ---------------------------------------
465
-
466
- const refusing = recipesRefusingSubmissions(validation, changedPaths);
467
- if (refusing.length > 0) {
468
- warn(describeRefusing(refusing));
469
- if (!(await questions.proceedDespiteSubmissions(refusing))) {
470
- return cancelled("You chose not to propose a change to those recipes.");
471
- }
472
- }
473
-
474
- // --- Which branch, and which proposal -------------------------------------
475
-
476
- /** Ends the run with nothing written, saying why. */
477
- function cancelled(reason: string): SubmitResult {
478
- notice(`${reason} Nothing was written.`);
479
- return {
480
- ...base,
481
- outcome: "cancelled",
482
- branch: options.branch ?? checkedOut ?? baseBranch,
483
- refusing,
484
- dryRun: true,
485
- };
486
- }
487
-
488
- // The plan for the branch: stay, switch to an existing one, or create one.
489
- let branch: string;
490
- let branchAction: "stay" | "switch" | "create";
491
- if (options.branch !== undefined && options.branch !== checkedOut) {
492
- branch = options.branch;
493
- branchAction = (await branchExists(rootDir, branch, { run })) ? "switch" : "create";
494
- } else if (checkedOut === undefined || checkedOut === baseBranch) {
495
- branch = options.branch ?? submitBranchName(now);
496
- branchAction = "create";
497
- } else {
498
- branch = checkedOut;
499
- branchAction = "stay";
500
- }
501
-
502
- let previous: ProposalSummary | undefined;
503
- if (branchAction !== "create") {
504
- previous = await lookUp(branch);
505
- } else if (tracksProposals && options.branch !== undefined) {
506
- // A named branch that does not exist here may still have been pushed from
507
- // another machine, with a proposal behind it.
508
- previous = await lookUp(branch);
509
- }
510
- if (!tracksProposals) {
511
- notice(
512
- `The '${provider.id}' provider cannot look for a ${proposalNoun} that is already open, ` +
513
- `so sous opens a new one. If this branch already has one, pushing updates it and ` +
514
- `opening another may be refused.`
515
- );
516
- }
517
-
518
- let action: "create" | "update" = previous?.state === "open" ? "update" : "create";
519
-
520
- if (previous?.state === "merged") {
521
- notice(
522
- `The ${proposalNoun} for '${branch}' was merged` +
523
- (previous.url === undefined ? "." : `: ${previous.url}.`) +
524
- " A merged branch takes no further changes, so the next change goes on a new branch."
525
- );
526
- const generated = submitBranchName(now);
527
- const choice = await questions.nextBranch(previous, generated);
528
- if (choice.kind === "cancel") {
529
- return cancelled("You chose not to continue on a new branch.");
530
- }
531
- branch = choice.kind === "name" ? choice.name.trim() : generated;
532
- branchAction = "create";
533
- action = "create";
534
- } else if (previous?.state === "closed") {
535
- notice(
536
- `The ${proposalNoun} for '${branch}' was closed without being merged` +
537
- (previous.url === undefined ? "." : `: ${previous.url}.`) +
538
- ` A fresh ${proposalNoun} is opened for the branch.`
539
- );
540
- }
541
-
542
- // --- The text ---------------------------------------------------------------
543
- //
544
- // A new proposal needs a title and a description written by the person
545
- // proposing it, and so does a commit sous makes for them; sous never derives
546
- // either from commit messages. An update takes them only when given.
547
-
548
- const needsText = action === "create" || toCommit.length > 0;
549
- let title = blankToUndefined(options.title);
550
- let description = blankToUndefined(options.body);
551
- if (needsText && title === undefined) title = (await questions.title()).trim();
552
- if (needsText && description === undefined) description = (await questions.body()).trim();
553
- if (needsText && (title === undefined || title.length === 0)) {
554
- throw new ConfigError(`A new ${proposalNoun} needs a title; pass '--title'.`);
555
- }
556
- if (needsText && (description === undefined || description.length === 0)) {
557
- throw new ConfigError(`A new ${proposalNoun} needs a description; pass '--body'.`);
558
- }
559
-
560
- if (toCommit.length > 0 && !(await questions.confirmCommit(toCommit))) {
561
- return cancelled("You chose not to commit those changes.");
562
- }
563
-
564
- // --- The changelog ----------------------------------------------------------
565
-
566
- const changelog = buildChangelog({
567
- baseBranch,
568
- base: since === undefined ? undefined : await readManifestsAt(rootDir, since, { run }),
569
- head: snapshotOf(validation),
570
- changedPaths,
571
- });
572
- const proposalBody =
573
- description === undefined ? undefined : composeProposalBody(description, changelog);
574
-
575
- // --- A dry run stops here ---------------------------------------------------
576
-
577
- if (dryRun) {
578
- if (branchAction === "create") {
579
- notice(`A branch named '${branch}' would be created from the current commit.`);
580
- } else if (branchAction === "switch") {
581
- notice(`The branch '${branch}' would be checked out.`);
582
- }
583
- if (toCommit.length > 0) {
584
- notice(`${describeCount(toCommit.length, "path")} would be committed.`);
585
- }
586
- if (usedFork) {
587
- notice(
588
- `You cannot push to ${repo.owner}/${repo.name}, so the change would go through a ` +
589
- `fork on your own account.`
590
- );
591
- }
592
- notice("Nothing was sent; this was a dry run.");
593
- return {
594
- ...base,
595
- outcome: action === "update" ? "updated" : "created",
596
- branch,
597
- ...(title === undefined ? {} : { title }),
598
- ...(previous === undefined ? {} : { previous }),
599
- ...(previous?.url === undefined || action !== "update" ? {} : { url: previous.url }),
600
- changelog,
601
- refusing,
602
- dryRun: true,
603
- };
604
- }
605
-
606
- // --- Write: the branch, the commit ------------------------------------------
607
-
608
- if (branchAction === "create") {
609
- await doStep(`Creating the branch '${branch}' from the current commit`, () =>
610
- createBranch(rootDir, branch, { run })
611
- );
612
- } else if (branchAction === "switch") {
613
- await doStep(`Checking out the branch '${branch}'`, () =>
614
- switchBranch(rootDir, branch, { run })
615
- );
616
- }
617
-
618
- if (toCommit.length > 0) {
619
- await doStep(`Committing ${describeCount(toCommit.length, "path")}`, () =>
620
- commitEverything(rootDir, commitMessage(title!, description!, changelog), { run })
621
- );
622
- }
623
-
624
- // --- Fork, push, propose ----------------------------------------------------
625
-
626
- let forkOwner = knownForkOwner;
627
- if (usedFork) {
628
- forkOwner = await prepareFork(rootDir, repo, provider, providerOptions, run, doStep);
629
- }
630
-
631
- const pushed = await doStep(`Pushing '${branch}' to '${pushRemote}'`, () =>
632
- pushBranch(rootDir, pushRemote, branch, { run }).catch((error: unknown) => {
633
- throw new ConfigError(
634
- `${error instanceof Error ? error.message : String(error)}\n` +
635
- ` Sous never forces a push. When the branch on '${pushRemote}' holds commits yours ` +
636
- `lacks (a maintainer may have pushed to it), bring them in with ` +
637
- `'git pull ${pushRemote} ${branch}', then run the command again.`
638
- );
639
- })
640
- );
641
-
642
- if (action === "update" && previous !== undefined && supportsProposals(provider)) {
643
- const replace = {
644
- ...(title === undefined ? {} : { title }),
645
- ...(proposalBody === undefined ? {} : { body: proposalBody }),
646
- };
647
- let url = previous.url;
648
- if (Object.keys(replace).length > 0) {
649
- const updated = await doStep(`Updating the ${proposalNoun}'s title and body`, () =>
650
- provider.updateProposal(repo, previous!.id, replace, providerOptions)
651
- );
652
- url = updated.url ?? url;
653
- }
654
- const changed = pushed === "updated" || Object.keys(replace).length > 0;
655
- const status = await statusOf(previous);
656
- return {
657
- ...base,
658
- outcome: changed ? "updated" : "unchanged",
659
- branch,
660
- ...(title === undefined ? {} : { title }),
661
- ...(url === undefined ? {} : { url }),
662
- previous,
663
- status,
664
- ...(toCommit.length > 0 ? { committed: toCommit.map((entry) => entry.path) } : {}),
665
- changelog,
666
- refusing,
667
- dryRun: false,
668
- };
669
- }
670
-
671
- const proposed = await doStep(`Opening a ${proposalNoun} for review`, () =>
672
- provider.proposeChange(
673
- repo,
674
- {
675
- branch,
676
- base: baseBranch,
677
- title: title!,
678
- body: proposalBody!,
679
- draft,
680
- ...(forkOwner === undefined ? {} : { head: { owner: forkOwner } }),
681
- },
682
- providerOptions
683
- )
684
- );
685
- if (proposed.url === undefined) notice(proposed.detail);
686
-
687
- return {
688
- ...base,
689
- outcome: "created",
690
- branch,
691
- title: title!,
692
- ...(proposed.url === undefined ? {} : { url: proposed.url }),
693
- ...(previous === undefined ? {} : { previous }),
694
- ...(toCommit.length > 0 ? { committed: toCommit.map((entry) => entry.path) } : {}),
695
- changelog,
696
- refusing,
697
- dryRun: false,
698
- };
699
- }
700
-
701
- // --- Preflight helpers --------------------------------------------------------------------------
702
-
703
- /** Refuses to submit a repository that does not describe itself correctly. */
704
- function assertRepoValidates(validation: RepoValidation): void {
705
- if (!hasErrors(validation.problems)) return;
706
- throw new ConfigError(
707
- "This repository does not validate, so there is nothing worth proposing yet:\n\n" +
708
- renderProblems(errorsIn(validation.problems)) +
709
- "\n\n Fix these, then run the command again."
710
- );
711
- }
712
-
713
- /** The refusal for a change that edits the index, naming how to put it back. */
714
- function indexEditedError(restore: string): ConfigError {
715
- return new ConfigError(
716
- `This change edits ${INDEX_FILENAME}. The index is written by the repository's own ` +
717
- `release, after a change is merged, so a proposal leaves it as it found it.\n` +
718
- ` Restore it with '${restore}', commit that, then run the command again.`
719
- );
720
- }
721
-
722
- /** Renders a list of problems as an indented block. */
723
- function renderProblems(problems: ReadonlyArray<ValidationProblem>): string {
724
- return problems.map((problem) => ` ${problem.where}: ${problem.message}`).join("\n");
725
- }
726
-
727
- /**
728
- * Says which recipes a change touches although they take no proposals, and
729
- * where each asks for changes to go instead.
730
- *
731
- * @param refusing - The recipes, as `recipesRefusingSubmissions` found them.
732
- */
733
- export function describeRefusing(refusing: ReadonlyArray<RefusingRecipe>): string {
734
- const lines = [
735
- refusing.length === 1
736
- ? "This change touches a recipe that does not take proposed changes:"
737
- : "This change touches recipes that do not take proposed changes:",
738
- ];
739
- for (const recipe of refusing) {
740
- const where =
741
- recipe.declaredBy === "recipe" ? "its own manifest" : "the repository manifest";
742
- lines.push(` ${recipe.key} (${recipe.path}), as ${where} says.`);
743
- if (recipe.instead !== undefined) lines.push(` Instead: ${recipe.instead}`);
744
- }
745
- lines.push(
746
- "Merging such a change usually breaks whatever publishes those recipes, and the " +
747
- "repository's own check refuses it."
748
- );
749
- return lines.join("\n");
750
- }
751
-
752
- /**
753
- * The provider that will carry the proposal, or a ConfigError pointing the
754
- * contributor at whatever route the repository documents instead. The feature
755
- * list is the only thing consulted: a provider that does not promise `submit`
756
- * is not asked to, whatever host it serves.
757
- */
758
- function requireSubmitProvider(
759
- upstreamUrl: string,
760
- validation: RepoValidation,
761
- providers?: RepoProvider[]
762
- ): SubmitCapableProvider {
763
- const provider =
764
- providers === undefined ? detectProvider(upstreamUrl) : detectProvider(upstreamUrl, providers);
765
-
766
- if (provider === undefined) {
767
- throw new ConfigError(
768
- `Sous does not know how to propose a change to ${upstreamUrl}.\n` +
769
- contributePointer(validation)
770
- );
771
- }
772
- if (!supportsSubmit(provider)) {
773
- throw new ConfigError(
774
- `The '${provider.id}' provider cannot propose a change on your behalf.\n` +
775
- contributePointer(validation)
776
- );
777
- }
778
- return provider;
779
- }
780
-
781
- /** The repository's own contribution instructions, when its manifest carries any. */
782
- function contributePointer(validation: RepoValidation): string {
783
- const contribute = validation.manifest.contribute;
784
- if (contribute === undefined) {
785
- return (
786
- ` This repository's manifest does not say where to send a change, so send it the ` +
787
- `way its maintainers prefer.`
788
- );
789
- }
790
- return ` This repository asks that changes be sent this way:\n ${contribute}`;
791
- }
792
-
793
- /** A string with its surrounding whitespace removed, or undefined when nothing is left. */
794
- function blankToUndefined(value: string | undefined): string | undefined {
795
- if (value === undefined) return undefined;
796
- const trimmed = value.trim();
797
- return trimmed.length === 0 ? undefined : trimmed;
798
- }
799
-
800
- /** "1 path", "3 paths". */
801
- function describeCount(count: number, noun: string): string {
802
- return `${count} ${noun}${count === 1 ? "" : "s"}`;
803
- }
804
-
805
- /**
806
- * The message `--commit` commits with: the title as the subject, then the
807
- * description, then the changelog.
808
- *
809
- * @param title - The proposal's title.
810
- * @param description - The contributor's description.
811
- * @param changelog - The generated changelog.
812
- */
813
- export function commitMessage(title: string, description: string, changelog: Changelog): string {
814
- return `${title}\n\n${description}\n\n${renderChangelog(changelog)}\n`;
815
- }
816
-
817
- // --- Delegation helpers -------------------------------------------------------------------------
818
-
819
- /**
820
- * The owner of the fork a `fork` remote already points at, which is the head
821
- * owner a proposal from it carries. Undefined when there is no such remote, or
822
- * when its URL is not one the provider can read.
823
- */
824
- async function forkOwnerFromRemote(
825
- rootDir: string,
826
- provider: RepoProvider,
827
- run: CommandRunner | undefined
828
- ): Promise<string | undefined> {
829
- const url = await remoteUrl(rootDir, FORK_REMOTE, { run });
830
- if (url === undefined || !provider.matches(url)) return undefined;
831
- try {
832
- return provider.canonicalize(url).owner;
833
- } catch {
834
- return undefined;
835
- }
836
- }
837
-
838
- /**
839
- * Asks the provider to fork the repository onto the contributor's own account,
840
- * then makes sure a git remote points at whatever came back. The fork itself is
841
- * the provider's business; the remote is git's, and therefore sous's.
842
- *
843
- * Returns the account the fork lives under, which is what a cross-repository
844
- * proposal needs.
845
- */
846
- async function prepareFork(
847
- rootDir: string,
848
- repo: CanonicalRepo,
849
- provider: SubmitCapableProvider,
850
- providerOptions: ProviderOptions,
851
- run: CommandRunner | undefined,
852
- doStep: <T>(message: string, action: () => Promise<T>) => Promise<T>
853
- ): Promise<string> {
854
- const fork = await doStep(`Forking ${repo.owner}/${repo.name} onto your own account`, () =>
855
- provider.fork(repo, providerOptions)
856
- );
857
-
858
- const existing = await remoteUrl(rootDir, FORK_REMOTE, { run });
859
- if (existing === undefined) {
860
- await doStep(`Adding the remote '${FORK_REMOTE}' for ${fork.owner}/${fork.name}`, async () => {
861
- try {
862
- await runGit(["remote", "add", FORK_REMOTE, fork.httpsUrl], { cwd: rootDir, run });
863
- } catch (error) {
864
- throw new ConfigError(
865
- `Could not add the remote '${FORK_REMOTE}'.\n ` +
866
- `${error instanceof Error ? error.message : String(error)}`
867
- );
868
- }
869
- });
870
- }
871
-
872
- return fork.owner;
873
- }
874
-
875
- /**
876
- * Turns a mid-flight failure into an error that says what already happened.
877
- * A pushed branch with no proposal behind it is a state the contributor has to
878
- * know about; silence would leave them guessing.
879
- */
880
- function partialStateError(
881
- failedStep: string,
882
- completed: ReadonlyArray<string>,
883
- error: unknown
884
- ): ConfigError {
885
- const done =
886
- completed.length === 0
887
- ? " nothing had been sent yet"
888
- : completed.map((entry) => ` ${entry}`).join("\n");
889
-
890
- return new ConfigError(
891
- `${failedStep}: this step failed.\n\n` +
892
- ` ${error instanceof Error ? error.message : String(error)}\n\n` +
893
- ` What had already been done:\n${done}\n\n` +
894
- ` Nothing after that step ran. Fix the problem above and run the command again; ` +
895
- `sous starts from where the repository actually is, so a step that already ` +
896
- `succeeded is not repeated.`
897
- );
898
- }