@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,899 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { Args, Flags } from "@oclif/core";
|
|
4
|
-
import { BaseCommand } from "../../base-command.js";
|
|
5
|
-
import { ConfigError } from "../../lib/errors.js";
|
|
6
|
-
import {
|
|
7
|
-
MANIFEST_EXTENSIONS,
|
|
8
|
-
REPO_MANIFEST_BASENAME,
|
|
9
|
-
} from "../../lib/repos/formats/common.js";
|
|
10
|
-
import { findRepoManifest } from "../../lib/repos/load-manifest.js";
|
|
11
|
-
import { enabledRepos } from "../../lib/repos/defaults.js";
|
|
12
|
-
import { subscriptionServiceFor } from "../../lib/repos/subscription-service.js";
|
|
13
|
-
import type { LinkOrigin } from "../../lib/repos/formats/links-map.js";
|
|
14
|
-
import {
|
|
15
|
-
assertBranchName,
|
|
16
|
-
cloneRepo,
|
|
17
|
-
compareWithUpstream,
|
|
18
|
-
createBranch,
|
|
19
|
-
currentBranch,
|
|
20
|
-
defaultBranch,
|
|
21
|
-
discardableWork,
|
|
22
|
-
fetchBranch,
|
|
23
|
-
generatedBranchName,
|
|
24
|
-
isGitCheckout,
|
|
25
|
-
lastFetchedAt,
|
|
26
|
-
localBranchExists,
|
|
27
|
-
looksLikeRepoUrl,
|
|
28
|
-
remoteUrlOf,
|
|
29
|
-
repoSlugFromUrl,
|
|
30
|
-
resetBranchToUpstream,
|
|
31
|
-
sameRemote,
|
|
32
|
-
switchBranch,
|
|
33
|
-
tryFetchUpstream,
|
|
34
|
-
UPSTREAM_REMOTE,
|
|
35
|
-
type DiscardableWork,
|
|
36
|
-
} from "../../lib/repos/git-clone.js";
|
|
37
|
-
import { isInteractive, nonInteractiveError } from "../../lib/interactive.js";
|
|
38
|
-
import { askYesNo } from "../../utils/prompts.js";
|
|
39
|
-
import {
|
|
40
|
-
assertLocalRepoDirectory,
|
|
41
|
-
expandHomePath,
|
|
42
|
-
looksLikeLocalPath,
|
|
43
|
-
resolveRepoArgument,
|
|
44
|
-
} from "../../lib/repos/providers/local.js";
|
|
45
|
-
import { readRepoManifestIn } from "../../lib/repos/locked-recipes.js";
|
|
46
|
-
import {
|
|
47
|
-
ensureReposIgnoreFiles,
|
|
48
|
-
globalReposDir,
|
|
49
|
-
projectReposDir,
|
|
50
|
-
readGlobalLinks,
|
|
51
|
-
readProjectLinks,
|
|
52
|
-
writeGlobalLinks,
|
|
53
|
-
writeProjectLinks,
|
|
54
|
-
} from "../../lib/repos/links.js";
|
|
55
|
-
import {
|
|
56
|
-
BULLET,
|
|
57
|
-
blankLine,
|
|
58
|
-
dryRunNotice,
|
|
59
|
-
footer,
|
|
60
|
-
heading,
|
|
61
|
-
log,
|
|
62
|
-
note,
|
|
63
|
-
paragraph,
|
|
64
|
-
showCommandVars,
|
|
65
|
-
showVariable,
|
|
66
|
-
showVariables,
|
|
67
|
-
warning,
|
|
68
|
-
} from "../../utils/formatting.js";
|
|
69
|
-
import { confirmationFlag } from "../../utils/flags.js";
|
|
70
|
-
import {
|
|
71
|
-
ensureGlobalReposDirectory,
|
|
72
|
-
ensureProjectReposDirectory,
|
|
73
|
-
} from "../../utils/sous-directory.js";
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* `sous repo link` points this project (or this machine) at a working copy of a
|
|
77
|
-
* repository instead of at a published version, which is how a maintainer edits
|
|
78
|
-
* recipes: edits happen in a checkout, never in the store.
|
|
79
|
-
*
|
|
80
|
-
* The command is written three ways. A path on its own links the checkout that
|
|
81
|
-
* is already at that path, in place. A repository name (or URL) on its own
|
|
82
|
-
* clones the repository into `.sous/repos/<owner>/<name>`, or into
|
|
83
|
-
* `$SOUS_HOME/repos/<owner>/<name>` with --global, where two projects can share
|
|
84
|
-
* one checkout. A name (or URL) followed by a path links the checkout at that
|
|
85
|
-
* path to that repository, and clones nothing.
|
|
86
|
-
*
|
|
87
|
-
* A linked repository's recipes are read from the checkout with no version, no
|
|
88
|
-
* lockfile and no hash check, so linking one is at least as consequential as
|
|
89
|
-
* adding one. Naming a repository this project has not added therefore runs the
|
|
90
|
-
* same trust ceremony `sous repo add` runs, rather than skipping it; there is no
|
|
91
|
-
* way to read from a repository this project does not trust.
|
|
92
|
-
*
|
|
93
|
-
* A link says nothing about WHY a checkout is being read (authoring, running a
|
|
94
|
-
* teammate's branch, a local fork, debugging), so the command never changes a
|
|
95
|
-
* checkout on its own. A checkout that was already on disk is fetched and
|
|
96
|
-
* compared with upstream, which changes none of its files or branches; every
|
|
97
|
-
* change to it is an explicit flag (`--branch`, `--create-branch`,
|
|
98
|
-
* `--generate-branch`, `--latest`), carried out by git, whose refusals are
|
|
99
|
-
* passed through. `--latest` is the one place sous checks for itself, because
|
|
100
|
-
* making a branch match upstream discards local work without git warning.
|
|
101
|
-
*/
|
|
102
|
-
export default class RepoLink extends BaseCommand {
|
|
103
|
-
static description = [
|
|
104
|
-
"Point a repository at a working copy on this machine instead of a published version",
|
|
105
|
-
"",
|
|
106
|
-
"'sous repo link <path>' links the checkout already at that path, where it is, " +
|
|
107
|
-
"adding the repository to this project first if it has not been added yet.",
|
|
108
|
-
"'sous repo link <name-or-url>' clones the repository into .sous/repos and links " +
|
|
109
|
-
"that clone.",
|
|
110
|
-
"'sous repo link <name-or-url> <path>' links the checkout at that path to that " +
|
|
111
|
-
"repository, and clones nothing.",
|
|
112
|
-
"A checkout that was already on disk is fetched and compared with upstream; " +
|
|
113
|
-
"only the branch flags and --latest change it.",
|
|
114
|
-
].join("\n");
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* The other spelling of the topic. It lives under a hidden topic, so it is
|
|
118
|
-
* typable everywhere without ever reaching the top-level listing.
|
|
119
|
-
*/
|
|
120
|
-
static aliases = ["repos:link"];
|
|
121
|
-
|
|
122
|
-
static examples = [
|
|
123
|
-
"<%= config.bin %> repo link ../sous-recipes",
|
|
124
|
-
"<%= config.bin %> repo link sous-recipes",
|
|
125
|
-
"<%= config.bin %> repo link sous-recipes ~/Projects/sous-recipes",
|
|
126
|
-
"<%= config.bin %> repo link https://github.com/sous-io/sous-recipes",
|
|
127
|
-
"<%= config.bin %> repo link sous-recipes --global",
|
|
128
|
-
"<%= config.bin %> repo link sous-recipes --latest",
|
|
129
|
-
"<%= config.bin %> repo link sous-recipes --generate-branch",
|
|
130
|
-
"<%= config.bin %> repo link sous-recipes --branch my-change --latest",
|
|
131
|
-
];
|
|
132
|
-
|
|
133
|
-
static args = {
|
|
134
|
-
repo: Args.string({
|
|
135
|
-
description:
|
|
136
|
-
"The repository's short name from this project's config, its full URL, or the " +
|
|
137
|
-
"path of a checkout to link in place",
|
|
138
|
-
required: true,
|
|
139
|
-
}),
|
|
140
|
-
path: Args.string({
|
|
141
|
-
description:
|
|
142
|
-
"An existing checkout to link. Without it, the repository is cloned.",
|
|
143
|
-
required: false,
|
|
144
|
-
}),
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
static flags = {
|
|
148
|
-
...BaseCommand.baseFlags,
|
|
149
|
-
global: Flags.boolean({
|
|
150
|
-
description:
|
|
151
|
-
"Link for every project on this machine, sharing one checkout, rather than for this project",
|
|
152
|
-
default: false,
|
|
153
|
-
}),
|
|
154
|
-
// Linking a repository this project has not added yet asks the trust
|
|
155
|
-
// question first; the shared confirmation flag answers it, under `--trust`
|
|
156
|
-
// as well as the usual spellings.
|
|
157
|
-
yes: confirmationFlag({ extraAliases: ["trust"] }),
|
|
158
|
-
"dry-run": Flags.boolean({
|
|
159
|
-
description: "Print what would change without cloning or writing anything",
|
|
160
|
-
default: false,
|
|
161
|
-
}),
|
|
162
|
-
branch: Flags.string({
|
|
163
|
-
description: "Switch the checkout to this existing branch, fetching it from upstream first",
|
|
164
|
-
helpValue: "<name>",
|
|
165
|
-
exclusive: ["create-branch", "generate-branch"],
|
|
166
|
-
}),
|
|
167
|
-
"create-branch": Flags.string({
|
|
168
|
-
description: "Create this new branch in the checkout and switch to it",
|
|
169
|
-
helpValue: "<name>",
|
|
170
|
-
exclusive: ["branch", "generate-branch"],
|
|
171
|
-
}),
|
|
172
|
-
"generate-branch": Flags.boolean({
|
|
173
|
-
description: "Create a new branch named sous/edit-<date>-<time> in the checkout and switch to it",
|
|
174
|
-
exclusive: ["branch", "create-branch"],
|
|
175
|
-
}),
|
|
176
|
-
from: Flags.string({
|
|
177
|
-
description:
|
|
178
|
-
"Start the new branch from this branch instead of the repository's default branch",
|
|
179
|
-
helpValue: "<branch>",
|
|
180
|
-
// oclif's `dependsOn` wants every listed flag, and these two exclude each
|
|
181
|
-
// other; `some` is its spelling of "at least one of".
|
|
182
|
-
relationships: [{ type: "some", flags: ["create-branch", "generate-branch"] }],
|
|
183
|
-
}),
|
|
184
|
-
latest: Flags.boolean({
|
|
185
|
-
description:
|
|
186
|
-
"Make the branch being worked from match upstream's, after listing any local work that would be discarded",
|
|
187
|
-
default: false,
|
|
188
|
-
}),
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
async run(): Promise<void> {
|
|
192
|
-
const { args, flags } = await this.parse(RepoLink);
|
|
193
|
-
const { sousDir } = this.configContext;
|
|
194
|
-
const isGlobal = flags.global;
|
|
195
|
-
const dryRun = flags["dry-run"];
|
|
196
|
-
|
|
197
|
-
// A path in the REPO slot means "link the checkout that is already here",
|
|
198
|
-
// so it settles both what is being linked and where it lives; a second path
|
|
199
|
-
// would have to contradict one of the two.
|
|
200
|
-
const checkout = this.checkoutInRepoSlot(args.repo);
|
|
201
|
-
if (checkout !== undefined && args.path !== undefined) {
|
|
202
|
-
throw new ConfigError(
|
|
203
|
-
`'${args.repo}' is the path of a checkout on this machine, so it already says ` +
|
|
204
|
-
`which checkout to link, and a second path cannot say it again.\n` +
|
|
205
|
-
` Link a checkout where it already is: sous repo link ${args.repo}\n` +
|
|
206
|
-
` Link a checkout to a named repository: sous repo link <name-or-url> ` +
|
|
207
|
-
`${args.path}\n` +
|
|
208
|
-
` Drop whichever of the two paths you did not mean.`
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const { name, url } = await this.resolveRepo(
|
|
213
|
-
args.repo,
|
|
214
|
-
flags.yes,
|
|
215
|
-
dryRun,
|
|
216
|
-
checkout
|
|
217
|
-
);
|
|
218
|
-
|
|
219
|
-
showCommandVars({
|
|
220
|
-
Project: this.projectLabel,
|
|
221
|
-
Repository: name,
|
|
222
|
-
Location: url ?? "(not needed; an existing checkout was given)",
|
|
223
|
-
Scope: isGlobal ? "this machine" : "this project",
|
|
224
|
-
"Dry Run": dryRun,
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
// The heading is not followed by a blank line here: the block that comes
|
|
228
|
-
// next opens with one of its own, and the trust ceremony this may run
|
|
229
|
-
// prints in between.
|
|
230
|
-
heading("Linking a repository");
|
|
231
|
-
|
|
232
|
-
// Both written forms that name a checkout link it exactly where it is; only
|
|
233
|
-
// a repository named on its own is cloned.
|
|
234
|
-
const existingCheckout = checkout ?? args.path;
|
|
235
|
-
const plan =
|
|
236
|
-
existingCheckout !== undefined
|
|
237
|
-
? this.planLinkToPath(existingCheckout)
|
|
238
|
-
: this.planClone(name, url, isGlobal, dryRun);
|
|
239
|
-
|
|
240
|
-
const request = branchRequestFrom(flags);
|
|
241
|
-
|
|
242
|
-
if (dryRun) {
|
|
243
|
-
blankLine();
|
|
244
|
-
dryRunNotice(`would link '${name}' to ${plan.directory}`);
|
|
245
|
-
for (const line of describeBranchRequest(request)) dryRunNotice(line);
|
|
246
|
-
dryRunNotice(
|
|
247
|
-
`would record it in ${isGlobal ? "the machine-wide" : "this project's"} links map`
|
|
248
|
-
);
|
|
249
|
-
footer();
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Branch work happens before the link is recorded, so a step git refuses
|
|
254
|
-
// leaves no link pointing at a checkout in a state nobody asked for.
|
|
255
|
-
if (hasBranchWork(request)) {
|
|
256
|
-
if (isGlobal) {
|
|
257
|
-
warning(
|
|
258
|
-
`This checkout is SHARED by every project on this machine that links '${name}' ` +
|
|
259
|
-
`with --global.\n` +
|
|
260
|
-
`Changing its branch changes what all of them build from.`
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
plan.notes.push(...(await this.applyBranchRequest(plan.directory, request, flags.yes)));
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const map = isGlobal ? readGlobalLinks() : readProjectLinks(sousDir);
|
|
267
|
-
const previous = map.links[name];
|
|
268
|
-
map.links[name] = {
|
|
269
|
-
path: plan.directory,
|
|
270
|
-
linkedAt: new Date().toISOString(),
|
|
271
|
-
origin: plan.origin,
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
const linksPath = isGlobal ? writeGlobalLinks(map) : writeProjectLinks(sousDir, map);
|
|
275
|
-
|
|
276
|
-
// Ignore hygiene runs for both scopes. Both files are idempotent, and a
|
|
277
|
-
// project that links anything at all wants its machine-local sous files kept
|
|
278
|
-
// out of version control whichever scope the link was recorded in.
|
|
279
|
-
ensureReposIgnoreFiles(sousDir);
|
|
280
|
-
|
|
281
|
-
blankLine();
|
|
282
|
-
for (const line of plan.notes) log(` ${line}`);
|
|
283
|
-
if (plan.notes.length > 0) blankLine();
|
|
284
|
-
|
|
285
|
-
const branch = isGitCheckout(plan.directory) ? currentBranch(plan.directory) : undefined;
|
|
286
|
-
showVariables({
|
|
287
|
-
Repository: name,
|
|
288
|
-
Checkout: plan.directory,
|
|
289
|
-
...(branch === undefined ? {} : { Branch: branch }),
|
|
290
|
-
"Recorded in": linksPath,
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
if (previous !== undefined && previous.path !== plan.directory) {
|
|
294
|
-
blankLine();
|
|
295
|
-
log(` This replaces an earlier link to ${previous.path}, which is untouched.`);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// A checkout sous has just cloned is as current as upstream by definition;
|
|
299
|
-
// any other one may be days or months old, so say how it compares.
|
|
300
|
-
if (plan.kind !== "cloned") this.reportUpstream(plan.directory);
|
|
301
|
-
|
|
302
|
-
warning(
|
|
303
|
-
`The repository '${name}' is now LINKED.\n` +
|
|
304
|
-
`Its recipes are read from the checkout above, so versions, the lockfile\n` +
|
|
305
|
-
`and freshness checks no longer apply to it. Builds say so every time.\n` +
|
|
306
|
-
`\n` +
|
|
307
|
-
`Run 'sous repo unlink ${name}${isGlobal ? " --global" : ""}' to go back to ` +
|
|
308
|
-
`published versions.`
|
|
309
|
-
);
|
|
310
|
-
|
|
311
|
-
footer();
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Carries out the branch flags on the checkout, in order: `--latest` first
|
|
316
|
-
* (which switches to the branch being worked from and makes it match
|
|
317
|
-
* upstream's), then `--branch` when `--latest` did not already switch to it,
|
|
318
|
-
* then the new branch. Git decides whether each step may happen; the one
|
|
319
|
-
* thing checked here is what `--latest` would discard, because making a
|
|
320
|
-
* branch match upstream discards work without git warning about it.
|
|
321
|
-
*
|
|
322
|
-
* @param directory - The checkout.
|
|
323
|
-
* @param request - What the flags asked for.
|
|
324
|
-
* @param yes - The confirmation flag, which answers the discard question.
|
|
325
|
-
* @returns Lines describing what was done, for the notes block.
|
|
326
|
-
*/
|
|
327
|
-
private async applyBranchRequest(
|
|
328
|
-
directory: string,
|
|
329
|
-
request: BranchRequest,
|
|
330
|
-
yes: boolean
|
|
331
|
-
): Promise<string[]> {
|
|
332
|
-
if (!isGitCheckout(directory)) {
|
|
333
|
-
throw new ConfigError(
|
|
334
|
-
`${directory} is not a git checkout, so it has no branches to switch or create.\n` +
|
|
335
|
-
` Link it without the branch flags, or turn it into a git repository first.`
|
|
336
|
-
);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
for (const name of [request.switchTo, request.create, request.from]) {
|
|
340
|
-
if (name !== undefined) assertBranchName(directory, name);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const hasUpstream = remoteUrlOf(directory) !== undefined;
|
|
344
|
-
const needsDefault =
|
|
345
|
-
(request.create !== undefined && request.from === undefined) ||
|
|
346
|
-
(request.latest && request.switchTo === undefined && request.from === undefined);
|
|
347
|
-
const fallback = needsDefault ? this.requireDefaultBranch(directory) : undefined;
|
|
348
|
-
const notes: string[] = [];
|
|
349
|
-
|
|
350
|
-
if (request.latest) {
|
|
351
|
-
const target = (request.switchTo ?? request.from ?? fallback)!;
|
|
352
|
-
if (!hasUpstream) {
|
|
353
|
-
throw new ConfigError(
|
|
354
|
-
`--latest makes '${target}' match upstream's, and this checkout has no ` +
|
|
355
|
-
`'${UPSTREAM_REMOTE}' remote to be upstream.\n` +
|
|
356
|
-
` Add one with 'git remote add ${UPSTREAM_REMOTE} <url>' in ${directory}, or ` +
|
|
357
|
-
`link without --latest.`
|
|
358
|
-
);
|
|
359
|
-
}
|
|
360
|
-
fetchBranch(directory, target);
|
|
361
|
-
const work = discardableWork(directory, target);
|
|
362
|
-
await this.confirmDiscard(target, work, yes);
|
|
363
|
-
resetBranchToUpstream(directory, target);
|
|
364
|
-
notes.push(`Made the branch '${target}' match ${UPSTREAM_REMOTE}/${target}.`);
|
|
365
|
-
} else if (request.switchTo !== undefined) {
|
|
366
|
-
// A branch that exists locally is switched to as it is; one that does not
|
|
367
|
-
// is fetched first, since a single-branch clone cannot see it otherwise.
|
|
368
|
-
if (hasUpstream && !localBranchExists(directory, request.switchTo)) {
|
|
369
|
-
fetchBranch(directory, request.switchTo);
|
|
370
|
-
}
|
|
371
|
-
switchBranch(directory, request.switchTo);
|
|
372
|
-
notes.push(`Switched to the branch '${request.switchTo}'.`);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
if (request.create !== undefined) {
|
|
376
|
-
const base = (request.from ?? fallback)!;
|
|
377
|
-
let startPoint = base;
|
|
378
|
-
if (hasUpstream) {
|
|
379
|
-
// With --latest the base was fetched a moment ago.
|
|
380
|
-
if (!request.latest) fetchBranch(directory, base);
|
|
381
|
-
startPoint = `${UPSTREAM_REMOTE}/${base}`;
|
|
382
|
-
}
|
|
383
|
-
createBranch(directory, request.create, startPoint);
|
|
384
|
-
notes.push(
|
|
385
|
-
request.generated
|
|
386
|
-
? `Created the branch '${request.create}' (a generated name) from ${startPoint}, ` +
|
|
387
|
-
`and switched to it.`
|
|
388
|
-
: `Created the branch '${request.create}' from ${startPoint}, and switched to it.`
|
|
389
|
-
);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
return notes;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* The upstream default branch, or a ConfigError saying it could not be
|
|
397
|
-
* worked out and which flag names a branch instead.
|
|
398
|
-
*
|
|
399
|
-
* @param directory - The checkout.
|
|
400
|
-
*/
|
|
401
|
-
private requireDefaultBranch(directory: string): string {
|
|
402
|
-
const found = defaultBranch(directory);
|
|
403
|
-
if (found !== undefined) return found;
|
|
404
|
-
throw new ConfigError(
|
|
405
|
-
`Could not work out the default branch of the repository checked out at ${directory}.\n` +
|
|
406
|
-
` git records it as '${UPSTREAM_REMOTE}/HEAD' when it clones, and this checkout ` +
|
|
407
|
-
`has no such record, nor could '${UPSTREAM_REMOTE}' be asked for it.\n` +
|
|
408
|
-
` Name the branch to work from with --from, or with --branch.`
|
|
409
|
-
);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
/**
|
|
413
|
-
* Lists what `--latest` would discard and asks once before going on. Nothing
|
|
414
|
-
* is asked when there is nothing to discard, and the confirmation flag
|
|
415
|
-
* answers the question ahead of time.
|
|
416
|
-
*
|
|
417
|
-
* @param branch - The branch being made to match upstream's.
|
|
418
|
-
* @param work - What would be discarded.
|
|
419
|
-
* @param yes - The confirmation flag.
|
|
420
|
-
*/
|
|
421
|
-
private async confirmDiscard(
|
|
422
|
-
branch: string,
|
|
423
|
-
work: DiscardableWork,
|
|
424
|
-
yes: boolean
|
|
425
|
-
): Promise<void> {
|
|
426
|
-
if (work.uncommitted.length === 0 && work.localCommits.length === 0) return;
|
|
427
|
-
|
|
428
|
-
blankLine();
|
|
429
|
-
paragraph(
|
|
430
|
-
`Making '${branch}' match ${UPSTREAM_REMOTE}/${branch} discards the local work below.`,
|
|
431
|
-
{ indent: 2 }
|
|
432
|
-
);
|
|
433
|
-
if (work.uncommitted.length > 0) {
|
|
434
|
-
blankLine();
|
|
435
|
-
showVariable("Uncommitted changes", work.uncommitted.length);
|
|
436
|
-
for (const line of work.uncommitted) log(` ${BULLET} ${line}`);
|
|
437
|
-
}
|
|
438
|
-
if (work.localCommits.length > 0) {
|
|
439
|
-
blankLine();
|
|
440
|
-
showVariable(`Commits ${UPSTREAM_REMOTE} does not have`, work.localCommits.length);
|
|
441
|
-
for (const line of work.localCommits) log(` ${BULLET} ${line}`);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
if (yes) return;
|
|
445
|
-
|
|
446
|
-
if (!isInteractive()) {
|
|
447
|
-
throw nonInteractiveError({
|
|
448
|
-
prompt: `whether to discard the local work on '${branch}' listed above`,
|
|
449
|
-
remedy:
|
|
450
|
-
"pass '--yes' (spelled '-y', '--force' or '--trust' if you prefer) to discard it " +
|
|
451
|
-
"without being asked.",
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
const proceed = await askYesNo("Discard it?");
|
|
456
|
-
if (!proceed) {
|
|
457
|
-
throw new ConfigError(
|
|
458
|
-
`Nothing was changed: the local work on '${branch}' was kept.\n` +
|
|
459
|
-
` The checkout is on the branch it was on before, and no link was recorded.`
|
|
460
|
-
);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Says how a checkout that was already on disk compares with upstream. A
|
|
466
|
-
* short fetch comes first; it updates only the remote-tracking refs, never
|
|
467
|
-
* the user's files or branches. When upstream cannot be reached, the link
|
|
468
|
-
* still stands and the warning says since when the checkout may have
|
|
469
|
-
* diverged.
|
|
470
|
-
*
|
|
471
|
-
* @param directory - The checkout.
|
|
472
|
-
*/
|
|
473
|
-
private reportUpstream(directory: string): void {
|
|
474
|
-
if (!isGitCheckout(directory)) return;
|
|
475
|
-
|
|
476
|
-
blankLine();
|
|
477
|
-
if (remoteUrlOf(directory) === undefined) {
|
|
478
|
-
note(
|
|
479
|
-
`The checkout has no '${UPSTREAM_REMOTE}' remote, so there is no upstream to ` +
|
|
480
|
-
`compare it with.`,
|
|
481
|
-
{ indent: 2 }
|
|
482
|
-
);
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
const fetched = tryFetchUpstream(directory);
|
|
487
|
-
const branch = defaultBranch(directory);
|
|
488
|
-
|
|
489
|
-
if (!fetched.ok) {
|
|
490
|
-
const since = lastFetchedAt(directory, branch);
|
|
491
|
-
warning(
|
|
492
|
-
`Could not reach upstream; the checkout may have diverged since ` +
|
|
493
|
-
(since === undefined
|
|
494
|
-
? `it was last fetched, and git has no record of when that was.`
|
|
495
|
-
: `${formatWhen(since)}, when it was last fetched.`) +
|
|
496
|
-
`\n\nGit said:\n${fetched.reason}\n\n` +
|
|
497
|
-
`The link was recorded all the same, and nothing in the checkout was changed.`
|
|
498
|
-
);
|
|
499
|
-
return;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
if (branch === undefined) {
|
|
503
|
-
note(
|
|
504
|
-
`Upstream was fetched, but its default branch could not be worked out, so there ` +
|
|
505
|
-
`is nothing to compare the checkout with.`,
|
|
506
|
-
{ indent: 2 }
|
|
507
|
-
);
|
|
508
|
-
return;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
const comparison = compareWithUpstream(directory, branch);
|
|
512
|
-
const upstream = `${UPSTREAM_REMOTE}/${branch}`;
|
|
513
|
-
const yesNo = (value: boolean | undefined) =>
|
|
514
|
-
value === undefined ? "unknown" : value ? "yes" : "no";
|
|
515
|
-
|
|
516
|
-
// The branch is already in the summary above; a detached HEAD has none
|
|
517
|
-
// there, so it is named here instead.
|
|
518
|
-
showVariables({
|
|
519
|
-
...(comparison.branch === undefined
|
|
520
|
-
? { "Checked out": `no branch; HEAD is detached at ${comparison.commit ?? "an unknown commit"}` }
|
|
521
|
-
: {}),
|
|
522
|
-
"Compared with": upstream,
|
|
523
|
-
[`Merged into ${upstream}`]: yesNo(comparison.merged),
|
|
524
|
-
[`Behind ${upstream}`]:
|
|
525
|
-
comparison.behind === undefined
|
|
526
|
-
? "unknown"
|
|
527
|
-
: `${comparison.behind} ${comparison.behind === 1 ? "commit" : "commits"}`,
|
|
528
|
-
});
|
|
529
|
-
blankLine();
|
|
530
|
-
note(`Upstream was fetched just now; nothing in the checkout was changed.`, {
|
|
531
|
-
indent: 2,
|
|
532
|
-
});
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
/**
|
|
536
|
-
* The checkout a REPO argument names outright, as an absolute path, or
|
|
537
|
-
* undefined when the argument is a short name or a URL instead.
|
|
538
|
-
*
|
|
539
|
-
* A repository this project has already added wins, because a short name is
|
|
540
|
-
* what a person types most often and a directory of the same name sitting in
|
|
541
|
-
* the working directory must not quietly take its place. Anything else that
|
|
542
|
-
* reads as a path is checked here rather than later: the directory has to
|
|
543
|
-
* exist and hold a repo manifest, and the message explains the path itself
|
|
544
|
-
* when it does not.
|
|
545
|
-
*
|
|
546
|
-
* @param input - The repo argument as the user typed it.
|
|
547
|
-
*/
|
|
548
|
-
private checkoutInRepoSlot(input: string): string | undefined {
|
|
549
|
-
if (enabledRepos(this.settings)[input] !== undefined) return undefined;
|
|
550
|
-
if (!looksLikeLocalPath(input)) return undefined;
|
|
551
|
-
|
|
552
|
-
const resolved = resolveRepoArgument(input);
|
|
553
|
-
assertLocalRepoDirectory(input, resolved);
|
|
554
|
-
return resolved;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
/**
|
|
558
|
-
* Works out which repository is being linked and where it lives. A short name
|
|
559
|
-
* is looked up in the project's `repos:` config, which is where `sous repo
|
|
560
|
-
* add` records a trusted repository.
|
|
561
|
-
*
|
|
562
|
-
* Neither a URL nor a path is taken on its own. Linking reads recipes straight
|
|
563
|
-
* out of a checkout, so either one, for a repository this project has not
|
|
564
|
-
* added, goes through `addRepo`, which is the trust ceremony: it asks (or
|
|
565
|
-
* requires `--trust`), writes the repository into the managed layer, and
|
|
566
|
-
* refuses outright when the short name it derives already belongs to a
|
|
567
|
-
* different repository. Only then is anything linked or cloned.
|
|
568
|
-
*
|
|
569
|
-
* @param input - The repo argument as the user typed it.
|
|
570
|
-
* @param trustFlag - The `--trust` flag, passed through to the ceremony.
|
|
571
|
-
* @param dryRun - When true, nothing is trusted, written or downloaded.
|
|
572
|
-
* @param checkout - The checkout the repo argument named, when it named one.
|
|
573
|
-
*/
|
|
574
|
-
private async resolveRepo(
|
|
575
|
-
input: string,
|
|
576
|
-
trustFlag: boolean,
|
|
577
|
-
dryRun: boolean,
|
|
578
|
-
checkout?: string
|
|
579
|
-
): Promise<{ name: string; url?: string }> {
|
|
580
|
-
const configured = enabledRepos(this.settings)[input];
|
|
581
|
-
if (configured !== undefined) {
|
|
582
|
-
return { name: input, url: configured.url };
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// A checkout named in the REPO slot is registered from where it already is;
|
|
586
|
-
// its own manifest suggests the short name, falling back to the directory's
|
|
587
|
-
// name, which is what `addRepo` uses when it is given none.
|
|
588
|
-
if (checkout !== undefined) {
|
|
589
|
-
const suggested = suggestedShortName(checkout);
|
|
590
|
-
const outcome = await this.addThroughCeremony(
|
|
591
|
-
checkout,
|
|
592
|
-
trustFlag,
|
|
593
|
-
dryRun,
|
|
594
|
-
suggested === undefined ? {} : { name: suggested }
|
|
595
|
-
);
|
|
596
|
-
return { name: outcome.name, url: outcome.url };
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
if (looksLikeRepoUrl(input)) {
|
|
600
|
-
const outcome = await this.addThroughCeremony(input, trustFlag, dryRun, {});
|
|
601
|
-
return { name: outcome.name, url: outcome.url };
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
const known = Object.keys(enabledRepos(this.settings)).sort();
|
|
605
|
-
const knownList =
|
|
606
|
-
known.length > 0
|
|
607
|
-
? ` This project knows about: ${known.join(", ")}.\n`
|
|
608
|
-
: " This project has no repositories configured yet.\n";
|
|
609
|
-
|
|
610
|
-
throw new ConfigError(
|
|
611
|
-
`'${input}' is not a repository this project knows about, and it is neither a URL ` +
|
|
612
|
-
`nor the path of a checkout on this machine.\n` +
|
|
613
|
-
knownList +
|
|
614
|
-
` Add the repository first with 'sous repo add <url>', then link it by its ` +
|
|
615
|
-
`short name.`
|
|
616
|
-
);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
/**
|
|
620
|
-
* Runs the trust ceremony for a repository this project has not added, and
|
|
621
|
-
* reports the name and location it was recorded under. Nothing is trusted,
|
|
622
|
-
* written or downloaded on a dry run.
|
|
623
|
-
*
|
|
624
|
-
* @param location - The repository's URL, or the absolute path of one on this machine.
|
|
625
|
-
* @param trustFlag - The `--trust` flag, passed through to the ceremony.
|
|
626
|
-
* @param dryRun - When true, work out what would happen and write nothing.
|
|
627
|
-
* @param naming - The short name to record it under, when one has been worked out.
|
|
628
|
-
*/
|
|
629
|
-
private async addThroughCeremony(
|
|
630
|
-
location: string,
|
|
631
|
-
trustFlag: boolean,
|
|
632
|
-
dryRun: boolean,
|
|
633
|
-
naming: { name?: string }
|
|
634
|
-
): Promise<{ name: string; url: string }> {
|
|
635
|
-
const service = subscriptionServiceFor({
|
|
636
|
-
configContext: this.configContext,
|
|
637
|
-
settings: this.settings,
|
|
638
|
-
shellEnv: this.shellEnv,
|
|
639
|
-
});
|
|
640
|
-
|
|
641
|
-
const outcome = await service.addRepo({
|
|
642
|
-
url: location,
|
|
643
|
-
...naming,
|
|
644
|
-
trust: trustFlag,
|
|
645
|
-
dryRun,
|
|
646
|
-
});
|
|
647
|
-
|
|
648
|
-
return { name: outcome.name, url: outcome.url };
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
/**
|
|
652
|
-
* Plans a link to a checkout that already exists. The directory must be there
|
|
653
|
-
* and must hold a repo manifest, because a directory without one is not a
|
|
654
|
-
* repository and linking it would fail later, further from the mistake.
|
|
655
|
-
*
|
|
656
|
-
* @param given - The path as the user typed it, resolved against the working directory.
|
|
657
|
-
*/
|
|
658
|
-
private planLinkToPath(given: string): LinkPlan {
|
|
659
|
-
const directory = path.resolve(process.cwd(), expandHomePath(given));
|
|
660
|
-
|
|
661
|
-
if (!fs.existsSync(directory)) {
|
|
662
|
-
throw new ConfigError(
|
|
663
|
-
`There is no directory at ${directory}.\n` +
|
|
664
|
-
` Pass the path of a checkout that already exists, or leave the path off ` +
|
|
665
|
-
`to have sous clone the repository for you.`
|
|
666
|
-
);
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
if (!fs.statSync(directory).isDirectory()) {
|
|
670
|
-
throw new ConfigError(
|
|
671
|
-
`${directory} is a file, not a directory.\n` +
|
|
672
|
-
` Pass the root directory of a repository checkout.`
|
|
673
|
-
);
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
const manifest = findRepoManifest(directory);
|
|
677
|
-
if (manifest === undefined) {
|
|
678
|
-
throw new ConfigError(
|
|
679
|
-
`${directory} is not a sous repository.\n` +
|
|
680
|
-
` A repository declares itself with a '${REPO_MANIFEST_BASENAME}` +
|
|
681
|
-
`${MANIFEST_EXTENSIONS[0]}' file at its root, and there is none there.\n` +
|
|
682
|
-
` Check the path, or create a repository with 'sous repo init'.`
|
|
683
|
-
);
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
return {
|
|
687
|
-
directory,
|
|
688
|
-
origin: "path",
|
|
689
|
-
kind: "path",
|
|
690
|
-
notes: [`Linked the checkout already at ${directory}.`],
|
|
691
|
-
};
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
/**
|
|
695
|
-
* Plans a link backed by a clone. A directory that is already a checkout of
|
|
696
|
-
* the same repository is reused rather than re-cloned, so running the command
|
|
697
|
-
* twice is harmless; a checkout of a different repository in the same place is
|
|
698
|
-
* an error, because silently reading the wrong recipes would be worse than
|
|
699
|
-
* stopping.
|
|
700
|
-
*
|
|
701
|
-
* @param name - The repository's short name.
|
|
702
|
-
* @param url - Where the repository lives.
|
|
703
|
-
* @param isGlobal - Whether the checkout is shared by every project on the machine.
|
|
704
|
-
* @param dryRun - When true, work the plan out but clone nothing.
|
|
705
|
-
*/
|
|
706
|
-
private planClone(
|
|
707
|
-
name: string,
|
|
708
|
-
url: string | undefined,
|
|
709
|
-
isGlobal: boolean,
|
|
710
|
-
dryRun: boolean
|
|
711
|
-
): LinkPlan {
|
|
712
|
-
if (url === undefined) {
|
|
713
|
-
throw new ConfigError(
|
|
714
|
-
`sous does not know where the repository '${name}' lives, so it cannot clone it.\n` +
|
|
715
|
-
` Add it with 'sous repo add <url>', or pass the path of a checkout that ` +
|
|
716
|
-
`already exists.`
|
|
717
|
-
);
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
const slug = repoSlugFromUrl(url);
|
|
721
|
-
const base = isGlobal ? globalReposDir() : projectReposDir(this.configContext.sousDir);
|
|
722
|
-
const directory = path.join(base, slug.owner, slug.name);
|
|
723
|
-
|
|
724
|
-
if (isGitCheckout(directory)) {
|
|
725
|
-
const existing = remoteUrlOf(directory);
|
|
726
|
-
if (existing !== undefined && !sameRemote(existing, url)) {
|
|
727
|
-
throw new ConfigError(
|
|
728
|
-
`${directory} is already a checkout of a different repository.\n` +
|
|
729
|
-
` It points at ${existing}, but '${name}' is ${url}.\n` +
|
|
730
|
-
` Move or delete that directory, or pass an explicit path to link the ` +
|
|
731
|
-
`checkout you meant.`
|
|
732
|
-
);
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
return {
|
|
736
|
-
directory,
|
|
737
|
-
origin: "clone",
|
|
738
|
-
kind: "reused",
|
|
739
|
-
notes: [
|
|
740
|
-
`Reused the checkout already at ${directory}; nothing was cloned.`,
|
|
741
|
-
],
|
|
742
|
-
};
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
if (dryRun) {
|
|
746
|
-
return {
|
|
747
|
-
directory,
|
|
748
|
-
origin: "clone",
|
|
749
|
-
kind: "cloned",
|
|
750
|
-
notes: [`Would clone ${url} into ${directory}.`],
|
|
751
|
-
};
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
// The directory holding the checkouts gets its README before the clone puts
|
|
755
|
-
// anything in it, whichever scope the link is for.
|
|
756
|
-
if (isGlobal) ensureGlobalReposDirectory(base);
|
|
757
|
-
else ensureProjectReposDirectory(base);
|
|
758
|
-
|
|
759
|
-
log(` Cloning ${url} into ${directory} ...`);
|
|
760
|
-
const clone = cloneRepo(url, directory);
|
|
761
|
-
|
|
762
|
-
const manifest = findRepoManifest(directory);
|
|
763
|
-
if (manifest === undefined) {
|
|
764
|
-
throw new ConfigError(
|
|
765
|
-
`${url} was cloned into ${directory}, but it is not a sous repository.\n` +
|
|
766
|
-
` A repository declares itself with a '${REPO_MANIFEST_BASENAME}` +
|
|
767
|
-
`${MANIFEST_EXTENSIONS[0]}' file at its root, and there is none there.\n` +
|
|
768
|
-
` The checkout has been left in place so you can look at it.`
|
|
769
|
-
);
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
const notes = [`Cloned ${url} into ${directory}.`];
|
|
773
|
-
if (clone.fellBackToFullClone) {
|
|
774
|
-
notes.push(
|
|
775
|
-
"That remote would not serve a shallow clone, so the full history was fetched."
|
|
776
|
-
);
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
return { directory, origin: "clone", kind: "cloned", notes };
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
/**
|
|
784
|
-
* The short name a checkout suggests for itself: the `name` its repo manifest
|
|
785
|
-
* declares. Undefined when the manifest cannot be read or validated, which
|
|
786
|
-
* leaves the caller with the directory's own name; a working copy is edited by
|
|
787
|
-
* hand and is allowed to be mid-change, so an unreadable manifest is not a
|
|
788
|
-
* reason to refuse the link.
|
|
789
|
-
*
|
|
790
|
-
* @param directory - The checkout's root directory.
|
|
791
|
-
*/
|
|
792
|
-
function suggestedShortName(directory: string): string | undefined {
|
|
793
|
-
try {
|
|
794
|
-
return readRepoManifestIn(directory)?.name;
|
|
795
|
-
} catch {
|
|
796
|
-
return undefined;
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
/** Where a link will point, how the working copy got there, and what to report. */
|
|
801
|
-
type LinkPlan = {
|
|
802
|
-
/** Absolute path to the working copy. */
|
|
803
|
-
directory: string;
|
|
804
|
-
/** Whether sous cloned it or was pointed at it. */
|
|
805
|
-
origin: LinkOrigin;
|
|
806
|
-
/**
|
|
807
|
-
* What this run did to get the checkout: cloned it just now, reused a clone
|
|
808
|
-
* already in place, or was given the path of one.
|
|
809
|
-
*/
|
|
810
|
-
kind: "cloned" | "reused" | "path";
|
|
811
|
-
/** Lines describing what happened, printed before the summary. */
|
|
812
|
-
notes: string[];
|
|
813
|
-
};
|
|
814
|
-
|
|
815
|
-
/** What the branch flags asked for, with a generated name already chosen. */
|
|
816
|
-
type BranchRequest = {
|
|
817
|
-
/** `--branch`: the existing branch to switch to. */
|
|
818
|
-
switchTo?: string;
|
|
819
|
-
/** `--create-branch` or `--generate-branch`: the branch to create. */
|
|
820
|
-
create?: string;
|
|
821
|
-
/** True when the name to create was generated rather than typed. */
|
|
822
|
-
generated: boolean;
|
|
823
|
-
/** `--from`: the base of the new branch; the default branch when unset. */
|
|
824
|
-
from?: string;
|
|
825
|
-
/** `--latest`: make the branch being worked from match upstream's. */
|
|
826
|
-
latest: boolean;
|
|
827
|
-
};
|
|
828
|
-
|
|
829
|
-
/**
|
|
830
|
-
* Reads the branch flags into one request. The generated name is chosen here,
|
|
831
|
-
* once, so the dry run and the real run would print the same thing.
|
|
832
|
-
*
|
|
833
|
-
* @param flags - The parsed flags.
|
|
834
|
-
*/
|
|
835
|
-
function branchRequestFrom(flags: {
|
|
836
|
-
branch?: string;
|
|
837
|
-
"create-branch"?: string;
|
|
838
|
-
"generate-branch"?: boolean;
|
|
839
|
-
from?: string;
|
|
840
|
-
latest: boolean;
|
|
841
|
-
}): BranchRequest {
|
|
842
|
-
const generated = flags["generate-branch"] === true;
|
|
843
|
-
const create = generated ? generatedBranchName() : flags["create-branch"];
|
|
844
|
-
return {
|
|
845
|
-
...(flags.branch === undefined ? {} : { switchTo: flags.branch }),
|
|
846
|
-
...(create === undefined ? {} : { create }),
|
|
847
|
-
generated,
|
|
848
|
-
...(flags.from === undefined ? {} : { from: flags.from }),
|
|
849
|
-
latest: flags.latest,
|
|
850
|
-
};
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
/** True when the request asks for anything to be done to the checkout. */
|
|
854
|
-
function hasBranchWork(request: BranchRequest): boolean {
|
|
855
|
-
return request.switchTo !== undefined || request.create !== undefined || request.latest;
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
/**
|
|
859
|
-
* What a dry run says the branch flags would do, one line per step, in the
|
|
860
|
-
* order the real run takes them.
|
|
861
|
-
*
|
|
862
|
-
* @param request - The branch request.
|
|
863
|
-
*/
|
|
864
|
-
function describeBranchRequest(request: BranchRequest): string[] {
|
|
865
|
-
const lines: string[] = [];
|
|
866
|
-
const workingFrom = request.switchTo ?? request.from ?? "the default branch";
|
|
867
|
-
const quoted = (name: string) => (name === "the default branch" ? name : `'${name}'`);
|
|
868
|
-
|
|
869
|
-
if (request.latest) {
|
|
870
|
-
lines.push(
|
|
871
|
-
`would fetch ${quoted(workingFrom)}, list any local work on it that would be ` +
|
|
872
|
-
`discarded, and make it match upstream's`
|
|
873
|
-
);
|
|
874
|
-
} else if (request.switchTo !== undefined) {
|
|
875
|
-
lines.push(`would switch the checkout to the branch '${request.switchTo}'`);
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
if (request.create !== undefined) {
|
|
879
|
-
lines.push(
|
|
880
|
-
`would create the branch '${request.create}' from upstream's ` +
|
|
881
|
-
`${quoted(request.from ?? "the default branch")} and switch to it`
|
|
882
|
-
);
|
|
883
|
-
}
|
|
884
|
-
return lines;
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
/**
|
|
888
|
-
* Renders a moment as `YYYY-MM-DD HH:MM` in local time, which is what a person
|
|
889
|
-
* reads a "since when" as.
|
|
890
|
-
*
|
|
891
|
-
* @param when - The moment to render.
|
|
892
|
-
*/
|
|
893
|
-
function formatWhen(when: Date): string {
|
|
894
|
-
const pad = (value: number) => String(value).padStart(2, "0");
|
|
895
|
-
return (
|
|
896
|
-
`${when.getFullYear()}-${pad(when.getMonth() + 1)}-${pad(when.getDate())} ` +
|
|
897
|
-
`${pad(when.getHours())}:${pad(when.getMinutes())}`
|
|
898
|
-
);
|
|
899
|
-
}
|