@sous-io/sous 0.2.37 → 1.0.2
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/README.md +14 -256
- package/dist/bin.js +11 -0
- package/dist/sous.js +1094 -0
- package/docs/markdown/README.md +5 -4
- package/docs/markdown/_sidebar.md +21 -1
- package/docs/markdown/commands.md +294 -431
- package/docs/markdown/config-discovery.md +25 -14
- package/docs/markdown/config-inspection.md +53 -31
- package/docs/markdown/config-layers.md +168 -66
- package/docs/markdown/config-variables.md +44 -11
- package/docs/markdown/configuration.md +104 -17
- package/docs/markdown/design-principles.md +4 -1
- package/docs/markdown/repositories-authoring.md +30 -0
- package/docs/markdown/repositories-consuming.md +72 -14
- package/docs/markdown/repositories-file-formats.md +31 -13
- package/docs/markdown/repositories-providers.md +48 -57
- package/docs/markdown/repositories-quickstart.md +3 -2
- package/docs/markdown/repositories-troubleshooting.md +27 -1
- package/docs/markdown/repositories-variables.md +6 -1
- package/docs/markdown/repositories.md +18 -8
- package/package.json +48 -107
- package/recipes/core/sous-skills/skills/about-liquid-templates/SKILL.tpl.md +4 -6
- package/recipes/core/sous-skills/skills/about-liquid-templates/references/liquid-filters.tpl.md +2 -0
- package/recipes/core/sous-skills/skills/about-sous/SKILL.tpl.md +10 -9
- package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +35 -14
- package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +51 -20
- package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
- package/sous.config.schema.json +351 -132
- package/bin/run.js +0 -51
- package/src/base-command.ts +0 -457
- package/src/commands/build.ts +0 -181
- package/src/commands/clear.ts +0 -79
- package/src/commands/compile.ts +0 -155
- package/src/commands/config/get.ts +0 -110
- package/src/commands/config/show.ts +0 -32
- package/src/commands/config/validate.ts +0 -53
- package/src/commands/help.ts +0 -46
- package/src/commands/init.ts +0 -289
- package/src/commands/launch.ts +0 -139
- package/src/commands/lock/rebuild.ts +0 -239
- package/src/commands/lock/show.ts +0 -115
- package/src/commands/namespace/list.ts +0 -138
- package/src/commands/namespace/show.ts +0 -130
- package/src/commands/prune.ts +0 -43
- package/src/commands/recipe/list.ts +0 -112
- package/src/commands/recipe/show.ts +0 -439
- package/src/commands/repo/add.ts +0 -145
- package/src/commands/repo/contribute.ts +0 -604
- package/src/commands/repo/gc.ts +0 -173
- package/src/commands/repo/init.ts +0 -136
- package/src/commands/repo/link.ts +0 -899
- package/src/commands/repo/list.ts +0 -236
- package/src/commands/repo/release.ts +0 -685
- package/src/commands/repo/remove.ts +0 -194
- package/src/commands/repo/search.ts +0 -243
- package/src/commands/repo/submit.ts +0 -341
- package/src/commands/repo/unlink.ts +0 -461
- package/src/commands/subscription/add.ts +0 -286
- package/src/commands/subscription/list.ts +0 -208
- package/src/commands/subscription/remove.ts +0 -182
- package/src/commands/subscription/update.ts +0 -216
- package/src/commands/update.ts +0 -178
- package/src/commands/vars/ask.ts +0 -417
- package/src/commands/vars/index.ts +0 -79
- package/src/commands/vars/list.ts +0 -67
- package/src/commands/vars/show.ts +0 -77
- package/src/config-command.ts +0 -30
- package/src/lib/build-preparation.ts +0 -128
- package/src/lib/build-service.ts +0 -580
- package/src/lib/config-discovery.ts +0 -378
- package/src/lib/config-inspect.ts +0 -145
- package/src/lib/config-kernel.mjs +0 -379
- package/src/lib/config-schema.ts +0 -401
- package/src/lib/env-file.ts +0 -328
- package/src/lib/env-local.ts +0 -212
- package/src/lib/errors.ts +0 -32
- package/src/lib/glob-pattern.ts +0 -24
- package/src/lib/include-resolver.ts +0 -523
- package/src/lib/interactive.ts +0 -165
- package/src/lib/markdown-compiler.ts +0 -947
- package/src/lib/package-info.ts +0 -25
- package/src/lib/pid-service.ts +0 -99
- package/src/lib/project-install.d.mts +0 -44
- package/src/lib/project-install.mjs +0 -234
- package/src/lib/project-scaffold/index.ts +0 -332
- package/src/lib/project-scaffold/templates.ts +0 -240
- package/src/lib/repos/catalog-display.ts +0 -215
- package/src/lib/repos/catalog-inputs.ts +0 -288
- package/src/lib/repos/catalog.ts +0 -842
- package/src/lib/repos/contribute.ts +0 -452
- package/src/lib/repos/core-recipe.ts +0 -105
- package/src/lib/repos/declarations.ts +0 -187
- package/src/lib/repos/defaults.ts +0 -175
- package/src/lib/repos/formats/common.ts +0 -440
- package/src/lib/repos/formats/index-file.ts +0 -253
- package/src/lib/repos/formats/links-map.ts +0 -98
- package/src/lib/repos/formats/lockfile.ts +0 -169
- package/src/lib/repos/formats/patterns.ts +0 -57
- package/src/lib/repos/formats/recipe-manifest.ts +0 -205
- package/src/lib/repos/formats/repo-manifest.ts +0 -97
- package/src/lib/repos/formats/store-entry.ts +0 -84
- package/src/lib/repos/formats/variable-definition.ts +0 -268
- package/src/lib/repos/freshness.ts +0 -264
- package/src/lib/repos/git-clone.ts +0 -850
- package/src/lib/repos/identity.ts +0 -89
- package/src/lib/repos/index.ts +0 -57
- package/src/lib/repos/links.ts +0 -356
- package/src/lib/repos/load-manifest.ts +0 -237
- package/src/lib/repos/lock-service.ts +0 -454
- package/src/lib/repos/locked-namespace-resolver.ts +0 -232
- package/src/lib/repos/locked-recipe-lookup.ts +0 -123
- package/src/lib/repos/locked-recipes.ts +0 -254
- package/src/lib/repos/managed-layer.ts +0 -422
- package/src/lib/repos/memories-hash-name.ts +0 -33
- package/src/lib/repos/namespace-resolver.ts +0 -527
- package/src/lib/repos/providers/base.ts +0 -268
- package/src/lib/repos/providers/git.ts +0 -245
- package/src/lib/repos/providers/github.ts +0 -589
- package/src/lib/repos/providers/gitlab.ts +0 -332
- package/src/lib/repos/providers/http.ts +0 -128
- package/src/lib/repos/providers/index-cache.ts +0 -421
- package/src/lib/repos/providers/index.ts +0 -106
- package/src/lib/repos/providers/local.ts +0 -410
- package/src/lib/repos/providers/provider.ts +0 -615
- package/src/lib/repos/recipe-config-layers.ts +0 -316
- package/src/lib/repos/recipe-key-matcher.ts +0 -62
- package/src/lib/repos/recipe-memories.ts +0 -178
- package/src/lib/repos/recipe-targets.ts +0 -236
- package/src/lib/repos/ref-search.ts +0 -46
- package/src/lib/repos/release/bump.ts +0 -161
- package/src/lib/repos/release/changelog.ts +0 -461
- package/src/lib/repos/release/git-state.ts +0 -460
- package/src/lib/repos/release/index-builder.ts +0 -1108
- package/src/lib/repos/release/index.ts +0 -19
- package/src/lib/repos/release/plan.ts +0 -521
- package/src/lib/repos/release/settle.ts +0 -154
- package/src/lib/repos/release/submissions.ts +0 -214
- package/src/lib/repos/release/submit-checkout.ts +0 -290
- package/src/lib/repos/release/submit-questions.ts +0 -153
- package/src/lib/repos/release/submit-service.ts +0 -898
- package/src/lib/repos/release/tags.ts +0 -239
- package/src/lib/repos/release/validate.ts +0 -463
- package/src/lib/repos/resolver.ts +0 -1028
- package/src/lib/repos/scaffold/index.ts +0 -245
- package/src/lib/repos/scaffold/templates.ts +0 -517
- package/src/lib/repos/seed.ts +0 -473
- package/src/lib/repos/store/contract.ts +0 -64
- package/src/lib/repos/store/hash.ts +0 -191
- package/src/lib/repos/store/recipe-store.ts +0 -599
- package/src/lib/repos/store/settings.ts +0 -58
- package/src/lib/repos/subscription-service.ts +0 -3859
- package/src/lib/repos/trust.ts +0 -447
- package/src/lib/repos/update-plan.ts +0 -235
- package/src/lib/self-update/index.ts +0 -14
- package/src/lib/self-update/installs.ts +0 -413
- package/src/lib/self-update/managers.ts +0 -126
- package/src/lib/self-update/plan.ts +0 -315
- package/src/lib/self-update/registry.ts +0 -99
- package/src/lib/self-update/run.ts +0 -506
- package/src/lib/self-update/versions.ts +0 -243
- package/src/lib/settings.ts +0 -1107
- package/src/lib/sous-home.ts +0 -104
- package/src/lib/state.ts +0 -167
- package/src/lib/vars/answers.ts +0 -192
- package/src/lib/vars/ask.ts +0 -1152
- package/src/lib/vars/definition-source.ts +0 -261
- package/src/lib/vars/display.ts +0 -231
- package/src/lib/vars/index.ts +0 -20
- package/src/lib/vars/ladder.ts +0 -282
- package/src/lib/vars/mappings.ts +0 -260
- package/src/lib/vars/names.ts +0 -94
- package/src/lib/vars/preanswers.ts +0 -395
- package/src/lib/vars/question-plan.ts +0 -241
- package/src/lib/vars/report.ts +0 -228
- package/src/lib/vars/safe-regex.ts +0 -235
- package/src/lib/vars/secrets.ts +0 -131
- package/src/lib/vars/validate.ts +0 -312
- package/src/lib/version-report.ts +0 -90
- package/src/lib/watch-loop.ts +0 -148
- package/src/lib/watch-service.ts +0 -116
- package/src/services/ref-resolver/container.ts +0 -107
- package/src/services/ref-resolver/dependency.ts +0 -60
- package/src/services/ref-resolver/format.ts +0 -197
- package/src/services/ref-resolver/glob.ts +0 -115
- package/src/services/ref-resolver/hash-names.ts +0 -155
- package/src/services/ref-resolver/index.ts +0 -55
- package/src/services/ref-resolver/injectable.ts +0 -36
- package/src/services/ref-resolver/location.ts +0 -62
- package/src/services/ref-resolver/lookups/cached-index-lookup.ts +0 -53
- package/src/services/ref-resolver/lookups/catalog-lookup.ts +0 -23
- package/src/services/ref-resolver/lookups/catalog-matcher.ts +0 -261
- package/src/services/ref-resolver/lookups/chained-lookup.ts +0 -27
- package/src/services/ref-resolver/lookups/env-var-lookup.ts +0 -65
- package/src/services/ref-resolver/lookups/fetched-index-lookup.ts +0 -150
- package/src/services/ref-resolver/lookups/recorded-dependency-lookup.ts +0 -62
- package/src/services/ref-resolver/lookups/ref-lookup.ts +0 -54
- package/src/services/ref-resolver/lookups/variable-lookup.ts +0 -123
- package/src/services/ref-resolver/named-refs.ts +0 -144
- package/src/services/ref-resolver/parser/env-var-splitter.ts +0 -27
- package/src/services/ref-resolver/parser/location-splitter.ts +0 -239
- package/src/services/ref-resolver/parser/name-path-splitter.ts +0 -200
- package/src/services/ref-resolver/parser/partial-ref.ts +0 -84
- package/src/services/ref-resolver/parser/query-splitter.ts +0 -61
- package/src/services/ref-resolver/parser/range-splitter.ts +0 -52
- package/src/services/ref-resolver/parser/ref-parser.ts +0 -115
- package/src/services/ref-resolver/parser/repo-qualifier-splitter.ts +0 -51
- package/src/services/ref-resolver/parts.ts +0 -115
- package/src/services/ref-resolver/pruners/command-line-pruner.ts +0 -47
- package/src/services/ref-resolver/pruners/include-pruner.ts +0 -66
- package/src/services/ref-resolver/pruners/manifest-pruner.ts +0 -130
- package/src/services/ref-resolver/pruners/ref-pruner.ts +0 -135
- package/src/services/ref-resolver/pruners/rule-helpers.ts +0 -44
- package/src/services/ref-resolver/pruners/stored-key-pruner.ts +0 -136
- package/src/services/ref-resolver/ref-helpers.ts +0 -62
- package/src/services/ref-resolver/ref-picker-service.ts +0 -204
- package/src/services/ref-resolver/ref-report.ts +0 -84
- package/src/services/ref-resolver/ref-resolve-arguments.ts +0 -47
- package/src/services/ref-resolver/ref-resolve-result.ts +0 -36
- package/src/services/ref-resolver/ref-resolver-service.ts +0 -323
- package/src/services/ref-resolver/source.ts +0 -48
- package/src/services/ref-resolver/tokens.ts +0 -25
- package/src/services/ref-resolver/types.ts +0 -155
- package/src/templating/filters/bullet-list.ts +0 -9
- package/src/templating/filters/index.ts +0 -8
- package/src/templating/init-liquid-engine.ts +0 -139
- package/src/templating/lib/glob-files.ts +0 -74
- package/src/templating/lib/import-export.ts +0 -32
- package/src/templating/lib/secret-scope.ts +0 -41
- package/src/templating/lib/tag-args.ts +0 -19
- package/src/templating/tags/exportScalarVarsJs.ts +0 -46
- package/src/templating/tags/getFiles.ts +0 -89
- package/src/templating/tags/index.ts +0 -14
- package/src/templating/tags/listFiles.ts +0 -54
- package/src/templating/tags/showVars.ts +0 -28
- package/src/utils/choice-prompt.ts +0 -143
- package/src/utils/command-errors.ts +0 -186
- package/src/utils/command-help.ts +0 -45
- package/src/utils/confirm-prompt.ts +0 -110
- package/src/utils/flags.ts +0 -177
- package/src/utils/formatting.ts +0 -849
- package/src/utils/prompts.ts +0 -53
- package/src/utils/sous-directory.ts +0 -245
- package/src/utils/table.ts +0 -603
- 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
|
-
}
|