@cldmv/git-embedded 1.0.0
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/LICENSE +201 -0
- package/README.md +145 -0
- package/bin/git-embedded.mjs +185 -0
- package/docs/design.md +122 -0
- package/docs/use-case-private-tests.md +97 -0
- package/hooks/_dispatch.template +67 -0
- package/hooks/reference-transaction +54 -0
- package/hooks/update-embedded-repos +56 -0
- package/messages/setup-bare-githooks.md +25 -0
- package/messages/setup-dispatcher-canonical-complete.md +30 -0
- package/messages/setup-dispatcher-missing-symlinks.md +56 -0
- package/messages/setup-dispatcher-non-conforming.md +35 -0
- package/messages/setup-husky.md +35 -0
- package/messages/setup-init-templatedir.md +21 -0
- package/messages/setup-lefthook.md +48 -0
- package/messages/setup-none.md +27 -0
- package/messages/setup-pre-commit.md +47 -0
- package/messages/setup-simple-git-hooks.md +40 -0
- package/messages/setup-system-hookspath.md +25 -0
- package/package.json +96 -0
- package/src/api/cli/doctor.mjs +15 -0
- package/src/api/cli/init.mjs +26 -0
- package/src/api/cli/install-hooks.mjs +124 -0
- package/src/api/cli/install-template.mjs +43 -0
- package/src/api/cli/link.mjs +42 -0
- package/src/api/cli/print-hook-script.mjs +35 -0
- package/src/api/cli/uninstall-hooks.mjs +21 -0
- package/src/api/cli/version.mjs +17 -0
- package/src/api/commander/custom-help.mjs +249 -0
- package/src/api/detect/dispatcher.mjs +218 -0
- package/src/api/detect/husky.mjs +23 -0
- package/src/api/detect/lefthook.mjs +37 -0
- package/src/api/detect/pre-commit.mjs +35 -0
- package/src/api/detect/run.mjs +71 -0
- package/src/api/detect/simple-git-hooks.mjs +26 -0
- package/src/api/git.mjs +59 -0
- package/src/api/install/dispatcher.mjs +51 -0
- package/src/api/install/hooks.mjs +80 -0
- package/src/api/install/template.mjs +15 -0
- package/src/api/link/batch.mjs +130 -0
- package/src/api/link/copy-executable.mjs +27 -0
- package/src/api/link/elevate-windows.mjs +52 -0
- package/src/api/log.mjs +38 -0
- package/src/api/messages/load.mjs +27 -0
- package/src/api/paths.mjs +43 -0
- package/src/api/prompt.mjs +26 -0
- package/src/api/report.mjs +83 -0
- package/src/lib/elevate-windows-child.mjs +43 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
## simple-git-hooks detected
|
|
2
|
+
|
|
3
|
+
This repo uses [simple-git-hooks](https://github.com/toplenboren/simple-git-hooks) for git hook management. Signal: `"simple-git-hooks"` key in `package.json`.
|
|
4
|
+
|
|
5
|
+
simple-git-hooks writes hook scripts directly into `.git/hooks/` from the `package.json` config. Running `npx simple-git-hooks` regenerates those files from the config. `git-embedded` cannot install into `.git/hooks/` without risk of being overwritten on the next regeneration.
|
|
6
|
+
|
|
7
|
+
## Why the CLI will not auto-integrate
|
|
8
|
+
|
|
9
|
+
`package.json` is the source of truth for simple-git-hooks. Editing `.git/hooks/<name>` directly is undone the next time the user runs `npx simple-git-hooks`. Modifying `package.json` is the correct integration path, but unattended JSON edits to a shared file (where developers may have local changes) is risky enough to avoid automating.
|
|
10
|
+
|
|
11
|
+
## Manual integration
|
|
12
|
+
|
|
13
|
+
Edit `package.json` to add `git-embedded`'s invocations to the `simple-git-hooks` block. Each entry is a shell command; chain ours with `&&` after any existing command:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"simple-git-hooks": {
|
|
18
|
+
"post-checkout": "npx git-embedded run-hook post-checkout \"$@\"",
|
|
19
|
+
"post-merge": "npx git-embedded run-hook post-merge \"$@\"",
|
|
20
|
+
"post-rewrite": "npx git-embedded run-hook post-rewrite \"$@\"",
|
|
21
|
+
"reference-transaction": "npx git-embedded run-hook reference-transaction \"$@\""
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
If you have existing entries for these hooks, chain them:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
"post-checkout": "your-existing-command && npx git-embedded run-hook post-checkout \"$@\""
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
After editing, run `npx simple-git-hooks` to regenerate `.git/hooks/`. Run `git embedded doctor` to confirm.
|
|
33
|
+
|
|
34
|
+
## Caveat about reference-transaction
|
|
35
|
+
|
|
36
|
+
simple-git-hooks may not support `reference-transaction` as a hook event in all versions. If `npx simple-git-hooks` ignores the entry, install that one hook outside simple-git-hooks (directly into `.git/hooks/reference-transaction`) — it lives at a separate hook name from the others, so simple-git-hooks won't touch it.
|
|
37
|
+
|
|
38
|
+
## Bring-your-own mode
|
|
39
|
+
|
|
40
|
+
`git embedded print-hook-script <name>` outputs the script body. You can use it via the `npx` indirection above, or inline its contents into the `package.json` command if you want to avoid the indirect call.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
## System-scope `core.hooksPath` detected
|
|
2
|
+
|
|
3
|
+
`git config --system core.hooksPath` is set on this machine to the path printed above. This is unusual — typically set by a system administrator, corporate management tool, or a Linux distribution's git packaging.
|
|
4
|
+
|
|
5
|
+
The contents of the system hooks path determine how this configuration affects `git-embedded`:
|
|
6
|
+
|
|
7
|
+
- If the system path contains a **dispatcher** that chains to `.git/hooks/`, behavior is the same as a global canonical dispatcher (setup #2). `git-embedded` can install per-repo hooks normally; the system dispatcher chains to them.
|
|
8
|
+
- If the system path contains **bare hook scripts** without a chain mechanism, anything `git-embedded` installs in `.git/hooks/` will never fire. Integration requires either modifying the system dispatcher (needs sudo / admin) or overriding `core.hooksPath` at a narrower scope (global or local).
|
|
9
|
+
|
|
10
|
+
## What the CLI proposes
|
|
11
|
+
|
|
12
|
+
- Print the classification of the system hooks path so you can see what's there.
|
|
13
|
+
- For dispatcher-style system setups: install per-repo hooks as normal. No system change needed.
|
|
14
|
+
- For non-dispatcher system setups: refuse to install and suggest one of:
|
|
15
|
+
- Override `core.hooksPath` at your user-global scope (`git config --global core.hooksPath …`) pointing at a personal dispatcher you control.
|
|
16
|
+
- Override `core.hooksPath` at the repo-local scope for just this repo.
|
|
17
|
+
- Escalate to your system administrator to convert the system dispatcher to a chaining model.
|
|
18
|
+
|
|
19
|
+
## Why the CLI will not modify system config
|
|
20
|
+
|
|
21
|
+
System-scope git config requires elevated privileges to change. Modifying it without explicit `--system --yes` flags risks breaking other users' workflows on shared machines. Even with such flags, blind modification of system config is the wrong default. The CLI will only ever advise.
|
|
22
|
+
|
|
23
|
+
## Permission considerations
|
|
24
|
+
|
|
25
|
+
If you do not have sudo or admin on this machine, the only available remediations are user-global or local overrides. Both work — the user-global override takes precedence for all your repos; the local override applies to just one repo. Pick based on whether you want this behavior in every repo you work in or just this one.
|
package/package.json
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cldmv/git-embedded",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Manage embedded git repositories (anonymous gitlinks) without .gitmodules. Provides hooks that restore standard git-command ergonomics for embedded children while keeping the child's origin URL out of the public parent repo.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20.19.0"
|
|
9
|
+
},
|
|
10
|
+
"author": {
|
|
11
|
+
"name": "Shinrai",
|
|
12
|
+
"company": "CLDMV",
|
|
13
|
+
"email": "git+npm@cldmv.net",
|
|
14
|
+
"url": "https://cldmv.net"
|
|
15
|
+
},
|
|
16
|
+
"contributors": [
|
|
17
|
+
{
|
|
18
|
+
"name": "CLDMV",
|
|
19
|
+
"url": "https://github.com/CLDMV"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"funding": {
|
|
23
|
+
"type": "github",
|
|
24
|
+
"url": "https://github.com/sponsors/shinrai"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/CLDMV/git-embedded.git"
|
|
33
|
+
},
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/CLDMV/git-embedded/issues"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/CLDMV/git-embedded#readme",
|
|
38
|
+
"keywords": [
|
|
39
|
+
"git",
|
|
40
|
+
"gitlink",
|
|
41
|
+
"submodule",
|
|
42
|
+
"anonymous-submodule",
|
|
43
|
+
"embedded",
|
|
44
|
+
"embedded-repo",
|
|
45
|
+
"private",
|
|
46
|
+
"hooks",
|
|
47
|
+
"hooks-installer",
|
|
48
|
+
"gitmodules-free",
|
|
49
|
+
"reference-transaction",
|
|
50
|
+
"post-checkout",
|
|
51
|
+
"post-merge",
|
|
52
|
+
"post-rewrite",
|
|
53
|
+
"dispatcher",
|
|
54
|
+
"cli"
|
|
55
|
+
],
|
|
56
|
+
"files": [
|
|
57
|
+
"bin",
|
|
58
|
+
"src",
|
|
59
|
+
"hooks",
|
|
60
|
+
"messages",
|
|
61
|
+
"docs",
|
|
62
|
+
"README.md",
|
|
63
|
+
"LICENSE"
|
|
64
|
+
],
|
|
65
|
+
"bin": {
|
|
66
|
+
"git-embedded": "./bin/git-embedded.mjs"
|
|
67
|
+
},
|
|
68
|
+
"scripts": {
|
|
69
|
+
"start": "node bin/git-embedded.mjs",
|
|
70
|
+
"test": "vitest run --config .configs/vitest.config.mjs",
|
|
71
|
+
"test:watch": "vitest --config .configs/vitest.config.mjs",
|
|
72
|
+
"coverage": "vitest run --coverage --config .configs/vitest.config.mjs",
|
|
73
|
+
"lint": "eslint --config .configs/eslint.config.mjs .",
|
|
74
|
+
"lint:fix": "eslint --config .configs/eslint.config.mjs . --fix",
|
|
75
|
+
"format": "prettier --config .configs/.prettierrc --write .",
|
|
76
|
+
"format:check": "prettier --config .configs/.prettierrc --check ."
|
|
77
|
+
},
|
|
78
|
+
"dependencies": {
|
|
79
|
+
"@cldmv/slothlet": "^3.7.0",
|
|
80
|
+
"@cldmv/wisp": "^1.0.1",
|
|
81
|
+
"chalk": "^5.4.1",
|
|
82
|
+
"commander": "^14.0.0",
|
|
83
|
+
"marked": "^15.0.12",
|
|
84
|
+
"marked-terminal": "^7.3.0"
|
|
85
|
+
},
|
|
86
|
+
"devDependencies": {
|
|
87
|
+
"@eslint/js": "^9.18.0",
|
|
88
|
+
"@eslint/json": "^0.10.0",
|
|
89
|
+
"@eslint/markdown": "^6.2.2",
|
|
90
|
+
"@vitest/coverage-v8": "^2.1.9",
|
|
91
|
+
"eslint": "^9.18.0",
|
|
92
|
+
"globals": "^15.14.0",
|
|
93
|
+
"prettier": "^3.4.2",
|
|
94
|
+
"vitest": "^2.1.9"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { self } from "@cldmv/slothlet/runtime";
|
|
2
|
+
|
|
3
|
+
export const spec = {
|
|
4
|
+
command: "doctor",
|
|
5
|
+
description: "Inspect the current environment and report what would happen on install. Takes no action.",
|
|
6
|
+
examples: ["$ git-embedded doctor"]
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export async function run() {
|
|
10
|
+
const result = await self.detect.run(process.cwd());
|
|
11
|
+
self.report.detectionHeader(result);
|
|
12
|
+
self.report.message(result.kind);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default { spec, run };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { self, context } from "@cldmv/slothlet/runtime";
|
|
2
|
+
|
|
3
|
+
export const spec = {
|
|
4
|
+
command: "init",
|
|
5
|
+
description: "Set up the current repo for embedded gitlinks: run install-hooks, then silence the 'embedded git repository' advice.",
|
|
6
|
+
options: [
|
|
7
|
+
["--no-symlinks", "Forwarded to install-hooks: use hard links instead of symbolic links."],
|
|
8
|
+
["--yes", "Forwarded to install-hooks: skip confirmation prompts."],
|
|
9
|
+
["--dispatcher-dir <path>", "Forwarded to install-hooks: override the default dispatcher directory."]
|
|
10
|
+
],
|
|
11
|
+
examples: ["$ git-embedded init", "$ git-embedded init --yes"]
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export async function run(opts = {}) {
|
|
15
|
+
await self.cli.installHooks.run(opts);
|
|
16
|
+
|
|
17
|
+
const { spawnSync } = context;
|
|
18
|
+
const cfg = spawnSync("git", ["config", "advice.addEmbeddedRepo", "false"], { encoding: "utf8" });
|
|
19
|
+
if (cfg.status === 0) {
|
|
20
|
+
self.report.success("Silenced 'embedded git repository' advice (git config advice.addEmbeddedRepo=false).");
|
|
21
|
+
} else {
|
|
22
|
+
self.report.warn(`Could not set git config advice.addEmbeddedRepo: ${cfg.stderr || cfg.stdout}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default { spec, run };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { self, context } from "@cldmv/slothlet/runtime";
|
|
2
|
+
import { CancelledByUser } from "../link/batch.mjs";
|
|
3
|
+
|
|
4
|
+
export const spec = {
|
|
5
|
+
command: "install-hooks",
|
|
6
|
+
description: "Install git-embedded's hook scripts into this repo, bootstrapping a global dispatcher if needed.",
|
|
7
|
+
options: [
|
|
8
|
+
["--no-symlinks", "Use hard links instead of symbolic links (avoids the Windows UAC prompt)."],
|
|
9
|
+
["--yes", "Skip confirmation prompts; assume yes for any modification."],
|
|
10
|
+
["--dispatcher-dir <path>", "Override the default dispatcher directory (~/.config/git/hooks)."]
|
|
11
|
+
],
|
|
12
|
+
examples: [
|
|
13
|
+
"$ git-embedded install-hooks",
|
|
14
|
+
"$ git-embedded install-hooks --no-symlinks",
|
|
15
|
+
"$ git-embedded install-hooks --yes"
|
|
16
|
+
]
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export async function run(opts = {}) {
|
|
20
|
+
const result = await self.detect.run(process.cwd());
|
|
21
|
+
self.report.detectionHeader(result);
|
|
22
|
+
self.report.message(result.kind);
|
|
23
|
+
self.report.plain("");
|
|
24
|
+
|
|
25
|
+
switch (result.action) {
|
|
26
|
+
case "install":
|
|
27
|
+
await installRepoOnly(result);
|
|
28
|
+
return;
|
|
29
|
+
case "heal-then-install":
|
|
30
|
+
await healAndInstall(result, opts);
|
|
31
|
+
return;
|
|
32
|
+
case "suggest-dispatcher":
|
|
33
|
+
await bootstrapAndInstall(result, opts);
|
|
34
|
+
return;
|
|
35
|
+
case "refuse":
|
|
36
|
+
self.report.error(`Refusing to install over ${result.kind}. See the guidance above for manual integration paths.`);
|
|
37
|
+
process.exit(2);
|
|
38
|
+
return;
|
|
39
|
+
default:
|
|
40
|
+
self.report.error(`Unknown detection action: ${result.action}`);
|
|
41
|
+
process.exit(2);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function installRepoOnly(result) {
|
|
46
|
+
const gitDir = result.paths.gitDir;
|
|
47
|
+
if (!gitDir) {
|
|
48
|
+
self.report.error("Not inside a git repository — run from inside the repo you want to install hooks for.");
|
|
49
|
+
process.exit(2);
|
|
50
|
+
}
|
|
51
|
+
const out = await self.install.hooks("install", gitDir);
|
|
52
|
+
if (out.installed.length > 0) self.report.success(`Installed per-repo hooks: ${out.installed.join(", ")}`);
|
|
53
|
+
for (const s of out.skipped) self.report.warn(`Skipped ${s.name}: ${s.reason}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function healAndInstall(result, opts) {
|
|
57
|
+
const missing = result.dispatcher.missing;
|
|
58
|
+
self.report.plain(context.chalk.bold("Proposed heal:"));
|
|
59
|
+
self.report.plain(` Dispatcher: ${result.dispatcher.dispatcherPath}`);
|
|
60
|
+
self.report.plain(` Add entries: ${missing.join(", ")}`);
|
|
61
|
+
self.report.plain(` Mechanism: ${opts.noSymlinks ? "hardlink" : "symlink (UAC on Windows when not elevated)"}`);
|
|
62
|
+
self.report.plain("");
|
|
63
|
+
const ok = await self.prompt.confirm("Add the missing entries?", { defaultYes: false, yes: opts.yes });
|
|
64
|
+
if (!ok) {
|
|
65
|
+
self.report.warn("Heal declined. Re-run after adding the entries yourself; see the message above.");
|
|
66
|
+
process.exit(2);
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const linkResult = await self.install.dispatcher(
|
|
70
|
+
"heal",
|
|
71
|
+
{ dispatcherPath: result.dispatcher.dispatcherPath, missing },
|
|
72
|
+
{ noSymlinks: opts.noSymlinks }
|
|
73
|
+
);
|
|
74
|
+
self.report.success(`Healed ${linkResult.created.length} entries`);
|
|
75
|
+
if (linkResult.fallbackToCopy.length > 0) self.report.warn(`Filesystem fallback to copy for ${linkResult.fallbackToCopy.length} entries`);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (err instanceof CancelledByUser) {
|
|
78
|
+
self.report.error(err.message);
|
|
79
|
+
self.report.plain("To heal without symlinks (no UAC prompt), re-run with --no-symlinks.");
|
|
80
|
+
process.exit(2);
|
|
81
|
+
}
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
await installRepoOnly(result);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function bootstrapAndInstall(result, opts) {
|
|
88
|
+
const dir = opts.dispatcherDir || self.paths.defaultGlobalDispatcherDir();
|
|
89
|
+
self.report.plain(context.chalk.bold("Proposed dispatcher install:"));
|
|
90
|
+
self.report.plain(` Directory: ${dir}`);
|
|
91
|
+
self.report.plain(` Link mechanism: ${opts.noSymlinks ? "hardlink" : "symlink (UAC on Windows when not elevated)"}`);
|
|
92
|
+
self.report.plain(` Global config: git config --global core.hooksPath ${dir}`);
|
|
93
|
+
self.report.plain("");
|
|
94
|
+
const ok = await self.prompt.confirm("Install the dispatcher and set global core.hooksPath?", { defaultYes: false, yes: opts.yes });
|
|
95
|
+
if (!ok) {
|
|
96
|
+
self.report.warn("Dispatcher install declined. Falling back to per-repo install.");
|
|
97
|
+
if (result.paths.gitDir) await installRepoOnly(result);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const out = await self.install.dispatcher("bootstrap", { dir }, { noSymlinks: opts.noSymlinks });
|
|
102
|
+
self.report.success(`Dispatcher installed at ${out.dispatcherPath}`);
|
|
103
|
+
const mechs = new Set(out.created.map((c) => c.mechanism));
|
|
104
|
+
self.report.plain(` Created ${out.created.length} entries (${[...mechs].join(", ")})`);
|
|
105
|
+
if (out.fallbackToCopy.length > 0) self.report.warn(`Filesystem fallback to copy for ${out.fallbackToCopy.length} entries`);
|
|
106
|
+
const gitConfig = context.spawnSync("git", ["config", "--global", "core.hooksPath", dir], { encoding: "utf8" });
|
|
107
|
+
if (gitConfig.status !== 0) {
|
|
108
|
+
self.report.error(`git config --global core.hooksPath failed: ${gitConfig.stderr || gitConfig.stdout}`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
self.report.success(`Set git config --global core.hooksPath ${dir}`);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
if (err instanceof CancelledByUser) {
|
|
114
|
+
self.report.error(err.message);
|
|
115
|
+
self.report.plain("To install without symlinks (no UAC prompt), re-run with --no-symlinks.");
|
|
116
|
+
process.exit(2);
|
|
117
|
+
}
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
if (result.paths.gitDir) await installRepoOnly(result);
|
|
121
|
+
else self.report.warn("Not inside a git repo — skipping per-repo hook install.");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export default { spec, run };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { self } from "@cldmv/slothlet/runtime";
|
|
2
|
+
|
|
3
|
+
export const spec = {
|
|
4
|
+
command: "install-template",
|
|
5
|
+
description: "Install git-embedded's hook scripts into git's init.templateDir/hooks so new repos start with them.",
|
|
6
|
+
options: [
|
|
7
|
+
["--template-dir <path>", "Override the template directory (default: git config --global init.templateDir)."],
|
|
8
|
+
["--force", "Overwrite existing hook files even if not owned by git-embedded."],
|
|
9
|
+
["--yes", "Skip confirmation prompts."]
|
|
10
|
+
],
|
|
11
|
+
examples: ["$ git-embedded install-template", "$ git-embedded install-template --template-dir ~/.config/git/template"]
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export async function run(opts = {}) {
|
|
15
|
+
const templateDir = opts.templateDir || self.git.getInitTemplateDir();
|
|
16
|
+
if (!templateDir) {
|
|
17
|
+
self.report.error("git config --global init.templateDir is not set, and no --template-dir was provided.");
|
|
18
|
+
self.report.plain("Set one first, e.g.: git config --global init.templateDir ~/.config/git/template");
|
|
19
|
+
process.exit(2);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
self.report.plain(`Template directory: ${templateDir}`);
|
|
23
|
+
self.report.plain(`Template hooks directory: ${templateDir}/hooks`);
|
|
24
|
+
self.report.plain("");
|
|
25
|
+
self.report.warn("Installing here only affects repos created or cloned AFTER this point.");
|
|
26
|
+
self.report.plain("Existing repos still need `git-embedded install-hooks` run individually.");
|
|
27
|
+
self.report.plain("");
|
|
28
|
+
|
|
29
|
+
const ok = await self.prompt.confirm(`Install git-embedded hook scripts into ${templateDir}/hooks?`, {
|
|
30
|
+
defaultYes: false,
|
|
31
|
+
yes: opts.yes
|
|
32
|
+
});
|
|
33
|
+
if (!ok) {
|
|
34
|
+
self.report.warn("Declined.");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const out = await self.install.template(templateDir, { force: opts.force });
|
|
39
|
+
if (out.installed.length > 0) self.report.success(`Installed template hooks: ${out.installed.join(", ")}`);
|
|
40
|
+
for (const s of out.skipped) self.report.warn(`Skipped ${s.name}: ${s.reason}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default { spec, run };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { self, context } from "@cldmv/slothlet/runtime";
|
|
2
|
+
|
|
3
|
+
export const spec = {
|
|
4
|
+
command: "link",
|
|
5
|
+
description:
|
|
6
|
+
"Clone a remote repo into <local-path> and stage it as an anonymous gitlink. Does NOT commit (you may want to stage other things in the same commit).",
|
|
7
|
+
args: [
|
|
8
|
+
["<local-path>", "Where to clone the child repo (created if missing)"],
|
|
9
|
+
["<remote-url>", "The child repo's clone URL (will NOT be recorded in .gitmodules)"]
|
|
10
|
+
],
|
|
11
|
+
examples: [
|
|
12
|
+
"$ git-embedded link tests git@example.com:org/private-tests.git",
|
|
13
|
+
"$ git-embedded link vendor/foo https://github.com/org/foo.git"
|
|
14
|
+
]
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function run(localPath, remoteUrl) {
|
|
18
|
+
const { fs, spawnSync } = context;
|
|
19
|
+
|
|
20
|
+
if (fs.existsSync(localPath)) {
|
|
21
|
+
self.report.error(`${localPath} already exists. Remove it or pick a different path before linking.`);
|
|
22
|
+
process.exit(2);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
self.report.plain(`Cloning ${remoteUrl} into ${localPath}…`);
|
|
26
|
+
const clone = spawnSync("git", ["clone", remoteUrl, localPath], { stdio: "inherit" });
|
|
27
|
+
if (clone.status !== 0) {
|
|
28
|
+
self.report.error(`git clone exited with status ${clone.status}`);
|
|
29
|
+
process.exit(clone.status || 1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const add = spawnSync("git", ["add", localPath], { stdio: "inherit" });
|
|
33
|
+
if (add.status !== 0) {
|
|
34
|
+
self.report.error(`git add ${localPath} exited with status ${add.status}`);
|
|
35
|
+
process.exit(add.status || 1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
self.report.success(`Staged gitlink at ${localPath} (no .gitmodules entry written).`);
|
|
39
|
+
self.report.plain("Commit when ready: git commit -m 'embed <name>'");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default { spec, run };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { self, context } from "@cldmv/slothlet/runtime";
|
|
2
|
+
|
|
3
|
+
const NAME_TO_SOURCE = {
|
|
4
|
+
"post-checkout": "update-embedded-repos",
|
|
5
|
+
"post-merge": "update-embedded-repos",
|
|
6
|
+
"post-rewrite": "update-embedded-repos",
|
|
7
|
+
"reference-transaction": "reference-transaction",
|
|
8
|
+
"update-embedded-repos": "update-embedded-repos",
|
|
9
|
+
_dispatch: "_dispatch.template",
|
|
10
|
+
dispatcher: "_dispatch.template"
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const spec = {
|
|
14
|
+
command: "print-hook-script",
|
|
15
|
+
description: "Print the contents of a packaged hook script to stdout. Useful for bring-your-own integrations.",
|
|
16
|
+
args: [["<name>", "Hook name (e.g. post-checkout, reference-transaction, update-embedded-repos, _dispatch)"]],
|
|
17
|
+
examples: [
|
|
18
|
+
"$ git-embedded print-hook-script post-checkout",
|
|
19
|
+
"$ git-embedded print-hook-script reference-transaction",
|
|
20
|
+
"$ git-embedded print-hook-script _dispatch"
|
|
21
|
+
]
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function run(name) {
|
|
25
|
+
const sourceName = NAME_TO_SOURCE[name];
|
|
26
|
+
if (!sourceName) {
|
|
27
|
+
self.report.error(`Unknown hook script: ${name}`);
|
|
28
|
+
self.report.error(`Known names: ${Object.keys(NAME_TO_SOURCE).sort().join(", ")}`);
|
|
29
|
+
process.exit(2);
|
|
30
|
+
}
|
|
31
|
+
const source = context.path.join(self.paths.hooksSourceDir(), sourceName);
|
|
32
|
+
process.stdout.write(context.fs.readFileSync(source, "utf8"));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default { spec, run };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { self } from "@cldmv/slothlet/runtime";
|
|
2
|
+
|
|
3
|
+
export const spec = {
|
|
4
|
+
command: "uninstall-hooks",
|
|
5
|
+
description: "Remove git-embedded's hook scripts from this repo's .git/hooks directory. Leaves other hooks untouched.",
|
|
6
|
+
examples: ["$ git-embedded uninstall-hooks"]
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export async function run() {
|
|
10
|
+
const gitDir = self.git.getGitDir(process.cwd());
|
|
11
|
+
if (!gitDir) {
|
|
12
|
+
self.report.error("Not inside a git repository.");
|
|
13
|
+
process.exit(2);
|
|
14
|
+
}
|
|
15
|
+
const out = await self.install.hooks("uninstall", gitDir);
|
|
16
|
+
if (out.removed.length === 0) self.report.plain("No git-embedded hooks found in this repo.");
|
|
17
|
+
else self.report.success(`Removed per-repo hooks: ${out.removed.join(", ")}`);
|
|
18
|
+
for (const k of out.kept) self.report.warn(`Left ${k.name} in place: ${k.reason}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default { spec, run };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { self, context } from "@cldmv/slothlet/runtime";
|
|
2
|
+
|
|
3
|
+
export const spec = {
|
|
4
|
+
command: "version",
|
|
5
|
+
aliases: ["-V", "--version"],
|
|
6
|
+
description: "Print git-embedded, node, and platform versions.",
|
|
7
|
+
examples: ["$ git-embedded version"]
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function run() {
|
|
11
|
+
const pkg = context.wispSync(context.path.join(self.paths.packageRoot(), "package.json"));
|
|
12
|
+
console.log(`${pkg.name} ${pkg.version}`);
|
|
13
|
+
console.log(`node ${process.version.replace(/^v/, "")}`);
|
|
14
|
+
console.log(`platform ${process.platform}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default { spec, run };
|