@nimbus-sh/worker 0.9.0 → 0.11.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/NOTICE.md +5 -0
- package/README.md +10 -0
- package/dist/_shared/session-router.d.ts +7 -0
- package/dist/_shared/session-router.d.ts.map +1 -1
- package/dist/_shared/session-router.js +7 -0
- package/dist/bindings/body.d.ts +6 -0
- package/dist/bindings/body.d.ts.map +1 -0
- package/dist/bindings/body.js +37 -0
- package/dist/bindings/kv.d.ts +0 -2
- package/dist/bindings/kv.d.ts.map +1 -1
- package/dist/bindings/kv.js +3 -42
- package/dist/bindings/r2.d.ts +0 -2
- package/dist/bindings/r2.d.ts.map +1 -1
- package/dist/bindings/r2.js +4 -44
- package/dist/esbuild-cli-artifact.generated.d.ts +18 -0
- package/dist/esbuild-cli-artifact.generated.d.ts.map +1 -0
- package/dist/esbuild-cli-artifact.generated.js +17 -0
- package/dist/esbuild-wasm-bundle.generated.d.ts +24 -24
- package/dist/esbuild-wasm-bundle.generated.d.ts.map +1 -1
- package/dist/esbuild-wasm-bundle.generated.js +24 -24
- package/dist/facets/cirrus-real.d.ts.map +1 -1
- package/dist/facets/cirrus-real.js +11 -6
- package/dist/facets/compose.d.ts +11 -7
- package/dist/facets/compose.d.ts.map +1 -1
- package/dist/facets/compose.js +8 -31
- package/dist/facets/data-plan.d.ts +79 -0
- package/dist/facets/data-plan.d.ts.map +1 -0
- package/dist/facets/data-plan.js +477 -0
- package/dist/facets/esbuild-bundle-pool.d.ts +1 -1
- package/dist/facets/esbuild-bundle-pool.js +3 -3
- package/dist/facets/esbuild-transform.d.ts +38 -8
- package/dist/facets/esbuild-transform.d.ts.map +1 -1
- package/dist/facets/esbuild-transform.js +219 -19
- package/dist/facets/exec-telemetry.d.ts +7 -3
- package/dist/facets/exec-telemetry.d.ts.map +1 -1
- package/dist/facets/manager.d.ts +198 -69
- package/dist/facets/manager.d.ts.map +1 -1
- package/dist/facets/manager.js +1276 -652
- package/dist/facets/opencode-staging.d.ts +0 -2
- package/dist/facets/opencode-staging.d.ts.map +1 -1
- package/dist/facets/opencode-staging.js +5 -11
- package/dist/facets/process.d.ts +4 -0
- package/dist/facets/process.d.ts.map +1 -1
- package/dist/facets/process.js +3 -0
- package/dist/facets/read-profile.d.ts +149 -0
- package/dist/facets/read-profile.d.ts.map +1 -0
- package/dist/facets/read-profile.js +483 -0
- package/dist/facets/real-vite-hmr.d.ts +2 -0
- package/dist/facets/real-vite-hmr.d.ts.map +1 -1
- package/dist/facets/real-vite-hmr.js +3 -2
- package/dist/facets/resident-identity.d.ts +0 -1
- package/dist/facets/resident-identity.d.ts.map +1 -1
- package/dist/facets/resident-identity.js +0 -3
- package/dist/facets/wasm-swap-registry.d.ts +34 -25
- package/dist/facets/wasm-swap-registry.d.ts.map +1 -1
- package/dist/facets/wasm-swap-registry.js +62 -56
- package/dist/facets/ws-terminal.d.ts +4 -4
- package/dist/facets/ws-terminal.d.ts.map +1 -1
- package/dist/facets/ws-terminal.js +4 -5
- package/dist/git/commands.d.ts +33 -10
- package/dist/git/commands.d.ts.map +1 -1
- package/dist/git/commands.js +1891 -197
- package/dist/git/network-facet.d.ts +12 -2
- package/dist/git/network-facet.d.ts.map +1 -1
- package/dist/git/network-facet.js +69 -18
- package/dist/git/unified-diff.d.ts +74 -0
- package/dist/git/unified-diff.d.ts.map +1 -0
- package/dist/git/unified-diff.js +676 -0
- package/dist/git-bundle.generated.d.ts +7 -11
- package/dist/git-bundle.generated.d.ts.map +1 -1
- package/dist/git-bundle.generated.js +7 -11
- package/dist/hosted/commands.d.ts.map +1 -1
- package/dist/hosted/commands.js +64 -245
- package/dist/hosted/runtime.d.ts +22 -15
- package/dist/hosted/runtime.d.ts.map +1 -1
- package/dist/hosted/runtime.js +72 -14
- package/dist/hosted/services.d.ts +8 -1
- package/dist/hosted/services.d.ts.map +1 -1
- package/dist/hosted/services.js +12 -13
- package/dist/hosted/session.d.ts +165 -0
- package/dist/hosted/session.d.ts.map +1 -0
- package/dist/hosted/session.js +132 -0
- package/dist/loaders/generated-workers.d.ts +6 -3
- package/dist/loaders/generated-workers.d.ts.map +1 -1
- package/dist/loaders/generated-workers.js +8 -5
- package/dist/loaders/npm-resolve-preamble.d.ts.map +1 -1
- package/dist/loaders/npm-resolve-preamble.js +0 -27
- package/dist/loaders/pre-bundle-preamble.d.ts +6 -3
- package/dist/loaders/pre-bundle-preamble.d.ts.map +1 -1
- package/dist/loaders/pre-bundle-preamble.js +13 -8
- package/dist/node-shims-artifact.generated.d.ts +13 -4
- package/dist/node-shims-artifact.generated.d.ts.map +1 -1
- package/dist/node-shims-artifact.generated.js +16 -7
- package/dist/npm/bin-links.d.ts +3 -1
- package/dist/npm/bin-links.d.ts.map +1 -1
- package/dist/npm/bin-links.js +32 -8
- package/dist/npm/cache.d.ts +11 -1
- package/dist/npm/cache.d.ts.map +1 -1
- package/dist/npm/cache.js +21 -7
- package/dist/npm/install-batch-facet.d.ts +4 -2
- package/dist/npm/install-batch-facet.d.ts.map +1 -1
- package/dist/npm/install-batch-facet.js +32 -18
- package/dist/npm/installer.d.ts +23 -3
- package/dist/npm/installer.d.ts.map +1 -1
- package/dist/npm/installer.js +330 -131
- package/dist/npm/npx-install.d.ts +3 -1
- package/dist/npm/npx-install.d.ts.map +1 -1
- package/dist/npm/npx-install.js +4 -1
- package/dist/npm/package-lock.d.ts +32 -0
- package/dist/npm/package-lock.d.ts.map +1 -0
- package/dist/npm/package-lock.js +65 -0
- package/dist/npm/placement.d.ts +16 -0
- package/dist/npm/placement.d.ts.map +1 -0
- package/dist/npm/placement.js +33 -0
- package/dist/npm/pre-bundle-facet.d.ts +1 -30
- package/dist/npm/pre-bundle-facet.d.ts.map +1 -1
- package/dist/npm/pre-bundle-facet.js +3 -5
- package/dist/npm/r2-cache.d.ts +12 -6
- package/dist/npm/r2-cache.d.ts.map +1 -1
- package/dist/npm/r2-cache.js +34 -16
- package/dist/npm/resolve-one-facet.d.ts +3 -0
- package/dist/npm/resolve-one-facet.d.ts.map +1 -1
- package/dist/npm/resolve-one-facet.js +15 -3
- package/dist/npm/resolver.d.ts +11 -13
- package/dist/npm/resolver.d.ts.map +1 -1
- package/dist/npm/resolver.js +12 -25
- package/dist/opencode-artifact.generated.js +2 -2
- package/dist/replica/routing.d.ts +2 -38
- package/dist/replica/routing.d.ts.map +1 -1
- package/dist/replica/routing.js +3 -55
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +15 -2
- package/dist/router/public-directory.d.ts +18 -0
- package/dist/router/public-directory.d.ts.map +1 -1
- package/dist/router/public-directory.js +1 -1
- package/dist/router/remote-api.d.ts +3 -1
- package/dist/router/remote-api.d.ts.map +1 -1
- package/dist/router/remote-api.js +42 -79
- package/dist/runtime/bash-repl.js +2 -2
- package/dist/runtime/bun-repl.d.ts.map +1 -1
- package/dist/runtime/bun-repl.js +2 -13
- package/dist/runtime/cpython-resident.d.ts.map +1 -1
- package/dist/runtime/cpython-resident.js +2 -10
- package/dist/runtime/esbuild-wasm-bytes.d.ts +37 -18
- package/dist/runtime/esbuild-wasm-bytes.d.ts.map +1 -1
- package/dist/runtime/esbuild-wasm-bytes.js +66 -92
- package/dist/runtime/facet-loader-host.d.ts.map +1 -1
- package/dist/runtime/facet-loader-host.js +0 -7
- package/dist/runtime/git-bundle-artifact.d.ts +17 -0
- package/dist/runtime/git-bundle-artifact.d.ts.map +1 -0
- package/dist/runtime/git-bundle-artifact.js +25 -0
- package/dist/runtime/node-repl.d.ts.map +1 -1
- package/dist/runtime/node-repl.js +2 -13
- package/dist/runtime/node-shims-artifact.d.ts +22 -23
- package/dist/runtime/node-shims-artifact.d.ts.map +1 -1
- package/dist/runtime/node-shims-artifact.js +48 -78
- package/dist/runtime/node-shims.d.ts.map +1 -1
- package/dist/runtime/node-shims.js +2358 -996
- package/dist/runtime/opencode-artifact.d.ts.map +1 -1
- package/dist/runtime/opencode-artifact.js +17 -53
- package/dist/runtime/opencode-facet-runner.d.ts +3 -6
- package/dist/runtime/opencode-facet-runner.d.ts.map +1 -1
- package/dist/runtime/opencode-facet-runner.js +45 -10
- package/dist/runtime/opentui-wasm-bytes.d.ts.map +1 -1
- package/dist/runtime/opentui-wasm-bytes.js +16 -49
- package/dist/runtime/package-manager.d.ts +0 -2
- package/dist/runtime/package-manager.d.ts.map +1 -1
- package/dist/runtime/package-manager.js +0 -3
- package/dist/runtime/python-repl.d.ts.map +1 -1
- package/dist/runtime/python-repl.js +4 -35
- package/dist/runtime/repl-session.d.ts +10 -0
- package/dist/runtime/repl-session.d.ts.map +1 -1
- package/dist/runtime/repl-session.js +7 -0
- package/dist/runtime/ruby-repl.d.ts.map +1 -1
- package/dist/runtime/ruby-repl.js +2 -2
- package/dist/runtime/ruby-resident.d.ts.map +1 -1
- package/dist/runtime/ruby-resident.js +41 -51
- package/dist/runtime/runtime-catalog.d.ts +10 -3
- package/dist/runtime/runtime-catalog.d.ts.map +1 -1
- package/dist/runtime/runtime-catalog.js +125 -15
- package/dist/runtime/sqlite-wasm-bytes.d.ts +4 -4
- package/dist/runtime/sqlite-wasm-bytes.d.ts.map +1 -1
- package/dist/runtime/sqlite-wasm-bytes.js +21 -57
- package/dist/runtime/staged-source.d.ts +67 -0
- package/dist/runtime/staged-source.d.ts.map +1 -0
- package/dist/runtime/staged-source.js +113 -0
- package/dist/runtime/wasi/preamble.d.ts +0 -22
- package/dist/runtime/wasi/preamble.d.ts.map +1 -1
- package/dist/runtime/wasi/preamble.js +98 -1425
- package/dist/runtime-catalog.generated.js +1 -1
- package/dist/session/agent.d.ts +14 -2
- package/dist/session/agent.d.ts.map +1 -1
- package/dist/session/agent.js +57 -19
- package/dist/session/ai.d.ts +0 -11
- package/dist/session/ai.d.ts.map +1 -1
- package/dist/session/ai.js +0 -4
- package/dist/session/fs-watch.d.ts +0 -11
- package/dist/session/fs-watch.d.ts.map +1 -1
- package/dist/session/fs-watch.js +0 -21
- package/dist/session/hibernation.d.ts +68 -2
- package/dist/session/hibernation.d.ts.map +1 -1
- package/dist/session/hibernation.js +95 -1
- package/dist/session/init-phases.d.ts +1 -2
- package/dist/session/init-phases.d.ts.map +1 -1
- package/dist/session/init-phases.js +1 -2
- package/dist/session/init.d.ts.map +1 -1
- package/dist/session/init.js +7 -20
- package/dist/session/keys.d.ts +0 -3
- package/dist/session/keys.d.ts.map +1 -1
- package/dist/session/keys.js +0 -3
- package/dist/session/legacy-reset.d.ts +11 -0
- package/dist/session/legacy-reset.d.ts.map +1 -0
- package/dist/session/legacy-reset.js +16 -0
- package/dist/session/nimbus-session.d.ts +50 -16
- package/dist/session/nimbus-session.d.ts.map +1 -1
- package/dist/session/nimbus-session.js +77 -71
- package/dist/session/npm-install-port.d.ts.map +1 -1
- package/dist/session/npm-install-port.js +2 -0
- package/dist/session/programmatic.d.ts +13 -18
- package/dist/session/programmatic.d.ts.map +1 -1
- package/dist/session/programmatic.js +53 -39
- package/dist/session/replica-routes.d.ts +2 -3
- package/dist/session/replica-routes.d.ts.map +1 -1
- package/dist/session/replica-routes.js +3 -10
- package/dist/session/routes.d.ts.map +1 -1
- package/dist/session/routes.js +11 -12
- package/dist/session/rpc.d.ts +108 -21
- package/dist/session/rpc.d.ts.map +1 -1
- package/dist/session/rpc.js +194 -39
- package/dist/session/start-real-vite.js +2 -2
- package/dist/session/state-store.d.ts +1 -17
- package/dist/session/state-store.d.ts.map +1 -1
- package/dist/session/state-store.js +3 -53
- package/dist/session/supervisor-op.d.ts +7 -0
- package/dist/session/supervisor-op.d.ts.map +1 -1
- package/dist/session/supervisor-op.js +6 -1
- package/dist/session/supervisor-rpc.d.ts +116 -19
- package/dist/session/supervisor-rpc.d.ts.map +1 -1
- package/dist/session/supervisor-rpc.js +194 -84
- package/dist/session/vite-command.d.ts.map +1 -1
- package/dist/session/vite-command.js +17 -14
- package/dist/session/ws.d.ts.map +1 -1
- package/dist/session/ws.js +20 -8
- package/dist/shell/npm-bin-entrypoints.d.ts.map +1 -1
- package/dist/shell/npm-bin-entrypoints.js +30 -5
- package/dist/vfs/facet-resident-limits.d.ts +33 -0
- package/dist/vfs/facet-resident-limits.d.ts.map +1 -0
- package/dist/vfs/facet-resident-limits.js +33 -0
- package/dist/vfs/facet-resident-store.d.ts +2 -10
- package/dist/vfs/facet-resident-store.d.ts.map +1 -1
- package/dist/vfs/facet-resident-store.js +1594 -219
- package/dist/workspace-host.d.ts +3 -1
- package/dist/workspace-host.d.ts.map +1 -1
- package/dist/workspace-host.js +2 -0
- package/dist/wrangler/nimbus-wrangler.d.ts +0 -2
- package/dist/wrangler/nimbus-wrangler.d.ts.map +1 -1
- package/dist/wrangler/nimbus-wrangler.js +8 -22
- package/package.json +34 -7
- package/public/_assets/esbuild-0.24.2.js +2380 -0
- package/public/_assets/opencode/1.16.2/index-attach.js +2 -2
- package/public/_assets/runtime/esbuild-cli-c4fda380d52befc1.js +6892 -0
- package/public/_assets/runtime/git-de3e60c9f3ff006a.js +103 -0
- package/public/_assets/runtime/{node-shims-acaa037e6d26ec0d.js → node-shims-06565b121555c194.js} +2879 -992
- package/public/_assets/runtime/resident-store-6ffb1939c53d982f.js +2264 -0
- package/public/_assets/runtime/vfs-write-ledger-e501e8fc793bc2c1.js +889 -0
- package/scripts/build-opencode-attach-entry.mjs +51 -1
- package/scripts/bundle-esbuild-wasm.mjs +55 -39
- package/scripts/bundle-facet-workers.mjs +159 -10
- package/scripts/bundle-git.mjs +88 -27
- package/scripts/bundle-node-shims.mjs +72 -46
- package/scripts/bundle-runtime.mjs +18 -421
- package/scripts/cf-git-patch.mjs +137 -0
- package/scripts/patch-install-deps.mjs +49 -96
- package/scripts/runtime-specs.mjs +435 -0
- package/vendor/git-http.generated.d.ts +82 -0
- package/vendor/git-upstream.generated.d.ts +4378 -0
- package/vendor/git.LICENSE +7 -0
- package/vendor/git.generated.d.mts +21 -0
- package/vendor/git.generated.mjs +103 -0
- package/dist/replica/suspension.d.ts +0 -36
- package/dist/replica/suspension.d.ts.map +0 -1
- package/dist/replica/suspension.js +0 -49
- package/dist/runtime/static-server.d.ts +0 -3
- package/dist/runtime/static-server.d.ts.map +0 -1
- package/dist/runtime/static-server.js +0 -133
- package/scripts/clean-dist.mjs +0 -7
package/dist/git/commands.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* git
|
|
2
|
+
* git/commands.ts — Nimbus v2.0 Git integration via isomorphic-git.
|
|
3
3
|
*
|
|
4
4
|
* Provides a full `git` command with subcommands:
|
|
5
|
-
* init, clone, status, add, commit, log, branch, checkout,
|
|
6
|
-
*
|
|
5
|
+
* init, clone, status, add, commit, log, branch, checkout, diff,
|
|
6
|
+
* ls-files, rev-parse, remote, fetch, pull, push, merge, reset, tag
|
|
7
7
|
*
|
|
8
8
|
* Uses a VFS→isomorphic-git FS adapter that maps all operations
|
|
9
9
|
* to the SqliteVFS.
|
|
@@ -11,18 +11,20 @@
|
|
|
11
11
|
import { requireVfsCred } from '@nimbus-sh/core/runtime/os-contracts.js';
|
|
12
12
|
import { execGitNetwork } from './network-facet.js';
|
|
13
13
|
import { normalizeVfsPath } from '@nimbus-sh/core/vfs/path.js';
|
|
14
|
-
import { dec } from '@nimbus-sh/core/_shared/bytes.js';
|
|
14
|
+
import { dec, enc } from '@nimbus-sh/core/_shared/bytes.js';
|
|
15
|
+
import { DEFAULT_CONTEXT, DEFAULT_RENAME_SCORE, absentSpec, bytesFromBinary, detectRenames, formatNameOnly, formatNameStatus, formatPatch, formatStat, parseRenameScore, pathLine, statFile, } from './unified-diff.js';
|
|
15
16
|
// ── Lazy-loaded isomorphic-git (avoid ~1MB load on every cold start) ────
|
|
16
17
|
// NOTE: local git ops (init, status, add, commit, log, branch, checkout,
|
|
17
|
-
// diff, remote, merge, reset, tag, config) run here in the supervisor DO.
|
|
18
|
+
// diff, ls-files, rev-parse, remote, merge, reset, tag, config) run here in the supervisor DO.
|
|
18
19
|
// Network ops (clone, fetch, pull) are delegated to the git-network-facet
|
|
19
20
|
// because the supervisor's CPU budget cannot handle packfile processing
|
|
20
21
|
// for real-world repos (>100 files).
|
|
21
22
|
let _git = null;
|
|
22
23
|
async function getGit() {
|
|
23
24
|
if (!_git) {
|
|
24
|
-
//
|
|
25
|
-
|
|
25
|
+
// Same patched build artifact used by the network facet. A fresh npm
|
|
26
|
+
// consumer does not run Nimbus' repository dependency-patching hook.
|
|
27
|
+
_git = (await import('../../vendor/git.generated.mjs')).git;
|
|
26
28
|
}
|
|
27
29
|
return _git;
|
|
28
30
|
}
|
|
@@ -46,20 +48,89 @@ function wantsUtf8(options) {
|
|
|
46
48
|
* Creates an isomorphic-git compatible `fs` object from SqliteVFS.
|
|
47
49
|
* isomorphic-git requires: readFile, writeFile, unlink, readdir,
|
|
48
50
|
* mkdir, rmdir, stat, lstat (all as promises).
|
|
51
|
+
*
|
|
52
|
+
* With a `worktree`, the adapter writes that worktree the way git's checkout
|
|
53
|
+
* does (entry.c create_directories, has_symlink_leading_path): below its top,
|
|
54
|
+
* `.git` aside, every leading component is lstat'd, and one that is not a real
|
|
55
|
+
* directory (a link, dangling or not, or a file) is replaced by one rather than
|
|
56
|
+
* followed, and a file replaces a link at its own path instead of writing
|
|
57
|
+
* through it. Components above the top are followed, as git follows them.
|
|
58
|
+
* Commands that only read the worktree or write `.git` pass no worktree.
|
|
49
59
|
*/
|
|
50
|
-
function createGitFs(vfs) {
|
|
51
|
-
// Path normalization is shared with esbuild-service via
|
|
60
|
+
function createGitFs(vfs, worktree = null) {
|
|
61
|
+
// Path normalization is shared with esbuild-service via @nimbus-sh/core/vfs/path.js.
|
|
52
62
|
// isomorphic-git constructs paths like `dir + '/' + filepath` which can
|
|
53
63
|
// produce `/home/user/project/.` or paths with `..` segments — those are
|
|
54
64
|
// collapsed before VFS lookup. The bounded `..` pop won't escape root.
|
|
55
65
|
const normalizePath = normalizeVfsPath;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
66
|
+
const top = worktree === null ? null : normalizePath(worktree);
|
|
67
|
+
const below = top ? `${top}/` : '';
|
|
68
|
+
const gitdir = `${below}.git`;
|
|
69
|
+
function lstatOrNull(p) {
|
|
70
|
+
try {
|
|
71
|
+
return vfs.lstat(p);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** A path git's checkout owns: inside the worktree, not its top, not in its .git. */
|
|
78
|
+
function checkedOut(p) {
|
|
79
|
+
return top !== null && p.startsWith(below) && p !== gitdir && !p.startsWith(`${gitdir}/`);
|
|
80
|
+
}
|
|
81
|
+
/** `p`'s directories, down to `p` itself when `self`; see createGitFs for the worktree's rule. */
|
|
82
|
+
function ensureDirectories(p, self) {
|
|
83
|
+
const parts = p.split('/');
|
|
84
|
+
for (let i = 1; i <= (self ? parts.length : parts.length - 1); i++) {
|
|
59
85
|
const dir = parts.slice(0, i).join('/');
|
|
60
|
-
if (
|
|
61
|
-
|
|
86
|
+
if (!dir)
|
|
87
|
+
continue;
|
|
88
|
+
if (!checkedOut(dir)) {
|
|
89
|
+
if (!vfs.exists(dir))
|
|
90
|
+
vfs.mkdir(dir, { recursive: true });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const st = lstatOrNull(dir);
|
|
94
|
+
if (st?.type === 'directory')
|
|
95
|
+
continue;
|
|
96
|
+
if (st)
|
|
97
|
+
vfs.unlink(dir);
|
|
98
|
+
vfs.mkdir(dir);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// The inode as Node's fs.Stats: git's stat cache compares ctime, ino, uid and gid too.
|
|
102
|
+
function statsOf(filepath, follow) {
|
|
103
|
+
const p = normalizePath(filepath);
|
|
104
|
+
let st;
|
|
105
|
+
if (!p) {
|
|
106
|
+
const now = Date.now();
|
|
107
|
+
st = { dev: 0, ino: 0, nlink: 1, type: 'directory', size: 0, atime: now, ctime: now, mtime: now, mode: 0o755, uid: 0, gid: 0 };
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
try {
|
|
111
|
+
st = follow ? vfs.stat(p) : vfs.lstat(p);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
const err = new Error(`ENOENT: no such file or directory, ${follow ? 'stat' : 'lstat'} '${filepath}'`);
|
|
115
|
+
err.code = 'ENOENT';
|
|
116
|
+
err.errno = -2;
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
62
119
|
}
|
|
120
|
+
const isDir = st.type === 'directory';
|
|
121
|
+
const isLink = st.type === 'symlink';
|
|
122
|
+
return {
|
|
123
|
+
isFile: () => st.type === 'file',
|
|
124
|
+
isDirectory: () => isDir,
|
|
125
|
+
isSymbolicLink: () => isLink,
|
|
126
|
+
size: st.size,
|
|
127
|
+
mode: (isLink ? 0o120000 : isDir ? 0o040000 : 0o100000) | (st.mode & 0o7777),
|
|
128
|
+
mtimeMs: st.mtime, mtime: new Date(st.mtime),
|
|
129
|
+
ctimeMs: st.ctime, ctime: new Date(st.ctime),
|
|
130
|
+
atimeMs: st.atime, atime: new Date(st.atime),
|
|
131
|
+
uid: st.uid, gid: st.gid, dev: st.dev, ino: st.ino, nlink: st.nlink,
|
|
132
|
+
type: isDir ? 'dir' : isLink ? 'symlink' : 'file',
|
|
133
|
+
};
|
|
63
134
|
}
|
|
64
135
|
return {
|
|
65
136
|
promises: {
|
|
@@ -81,7 +152,13 @@ function createGitFs(vfs) {
|
|
|
81
152
|
},
|
|
82
153
|
async writeFile(filepath, data, opts) {
|
|
83
154
|
const p = normalizePath(filepath);
|
|
84
|
-
|
|
155
|
+
ensureDirectories(p, false);
|
|
156
|
+
// A file replaces a link or a directory at its own path (entry.c checkout_entry, remove_subtree).
|
|
157
|
+
const existing = checkedOut(p) ? lstatOrNull(p)?.type : undefined;
|
|
158
|
+
if (existing === 'symlink')
|
|
159
|
+
vfs.unlink(p);
|
|
160
|
+
else if (existing === 'directory')
|
|
161
|
+
vfs.removeRecursive(p);
|
|
85
162
|
if (typeof data === 'string') {
|
|
86
163
|
vfs.writeFile(p, data);
|
|
87
164
|
}
|
|
@@ -91,7 +168,7 @@ function createGitFs(vfs) {
|
|
|
91
168
|
},
|
|
92
169
|
async unlink(filepath) {
|
|
93
170
|
const p = normalizePath(filepath);
|
|
94
|
-
if (
|
|
171
|
+
if (lstatOrNull(p))
|
|
95
172
|
vfs.unlink(p);
|
|
96
173
|
},
|
|
97
174
|
async readdir(filepath) {
|
|
@@ -103,79 +180,82 @@ function createGitFs(vfs) {
|
|
|
103
180
|
return vfs.readdir(p).map(e => e.name);
|
|
104
181
|
},
|
|
105
182
|
async mkdir(filepath, opts) {
|
|
106
|
-
|
|
107
|
-
if (!vfs.exists(p))
|
|
108
|
-
vfs.mkdir(p, { recursive: true });
|
|
183
|
+
ensureDirectories(normalizePath(filepath), true);
|
|
109
184
|
},
|
|
110
185
|
async rmdir(filepath) {
|
|
111
186
|
const p = normalizePath(filepath);
|
|
112
|
-
|
|
113
|
-
|
|
187
|
+
const st = lstatOrNull(p);
|
|
188
|
+
if (!st)
|
|
189
|
+
return;
|
|
190
|
+
// rmdir(2) of a link is ENOTDIR: it never removes the directory the link names.
|
|
191
|
+
if (st.type !== 'directory') {
|
|
192
|
+
throw Object.assign(new Error(`ENOTDIR: not a directory, rmdir '${filepath}'`), { code: 'ENOTDIR', errno: -20 });
|
|
193
|
+
}
|
|
194
|
+
vfs.rmdir(p);
|
|
114
195
|
},
|
|
115
196
|
async stat(filepath) {
|
|
116
|
-
|
|
117
|
-
// Synthetic directory stat — used for root, '.', and known directories
|
|
118
|
-
function dirStat() {
|
|
119
|
-
const now = Date.now();
|
|
120
|
-
const d = new Date(now);
|
|
121
|
-
return {
|
|
122
|
-
isFile: () => false, isDirectory: () => true, isSymbolicLink: () => false,
|
|
123
|
-
size: 0, mode: 0o755, type: 'dir',
|
|
124
|
-
mtimeMs: now, mtime: d, ctimeMs: now, ctime: d, atimeMs: now, atime: d,
|
|
125
|
-
uid: 1000, gid: 1000, dev: 0, ino: 0, nlink: 1,
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
// Empty path (from '.', '/', etc.) = root directory
|
|
129
|
-
if (!p)
|
|
130
|
-
return dirStat();
|
|
131
|
-
// Check if path is a known directory (even without VFS stat entry)
|
|
132
|
-
if (vfs.exists(p) && vfs.isDirectory(p))
|
|
133
|
-
return dirStat();
|
|
134
|
-
let st;
|
|
135
|
-
try {
|
|
136
|
-
st = vfs.stat(p);
|
|
137
|
-
}
|
|
138
|
-
catch {
|
|
139
|
-
const err = new Error(`ENOENT: no such file or directory, stat '${filepath}'`);
|
|
140
|
-
err.code = 'ENOENT';
|
|
141
|
-
err.errno = -2;
|
|
142
|
-
throw err;
|
|
143
|
-
}
|
|
144
|
-
// isomorphic-git calls .valueOf() on mtime/ctime/atime — all must be Date objects
|
|
145
|
-
const mtimeMs = st.mtime || Date.now();
|
|
146
|
-
const mtime = new Date(mtimeMs);
|
|
147
|
-
return {
|
|
148
|
-
isFile: () => st.type === 'file',
|
|
149
|
-
isDirectory: () => st.type === 'directory',
|
|
150
|
-
isSymbolicLink: () => false,
|
|
151
|
-
size: st.size,
|
|
152
|
-
mode: st.mode || 0o644,
|
|
153
|
-
mtimeMs,
|
|
154
|
-
mtime,
|
|
155
|
-
ctimeMs: mtimeMs,
|
|
156
|
-
ctime: mtime,
|
|
157
|
-
atimeMs: mtimeMs,
|
|
158
|
-
atime: mtime,
|
|
159
|
-
uid: 1000,
|
|
160
|
-
gid: 1000,
|
|
161
|
-
dev: 0,
|
|
162
|
-
ino: 0,
|
|
163
|
-
nlink: 1,
|
|
164
|
-
type: st.type === 'directory' ? 'dir' : 'file',
|
|
165
|
-
};
|
|
197
|
+
return statsOf(filepath, true);
|
|
166
198
|
},
|
|
167
199
|
async lstat(filepath) {
|
|
168
|
-
return
|
|
200
|
+
return statsOf(filepath, false);
|
|
169
201
|
},
|
|
170
202
|
async chmod() { },
|
|
171
|
-
async symlink() {
|
|
172
|
-
|
|
203
|
+
async symlink(target, filepath) {
|
|
204
|
+
const p = normalizePath(filepath);
|
|
205
|
+
ensureDirectories(p, false);
|
|
206
|
+
// Checkout retargets a link in place, as the clone facet's adapter does.
|
|
207
|
+
const st = lstatOrNull(p);
|
|
208
|
+
if (st?.type === 'directory' && checkedOut(p))
|
|
209
|
+
vfs.removeRecursive(p);
|
|
210
|
+
else if (st && st.type !== 'directory')
|
|
211
|
+
vfs.unlink(p);
|
|
212
|
+
vfs.symlink(target, p);
|
|
213
|
+
},
|
|
214
|
+
async readlink(filepath) {
|
|
215
|
+
return vfs.readlink(normalizePath(filepath));
|
|
216
|
+
},
|
|
173
217
|
},
|
|
174
218
|
};
|
|
175
219
|
}
|
|
176
220
|
function getDir(ctx) {
|
|
177
221
|
return '/' + (ctx.cwd || '/home/user').replace(/^\/+/, '');
|
|
178
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* The options git accepts BEFORE the subcommand. `-C <path>` runs the
|
|
225
|
+
* command as if started from <path>; repeated, each is relative to the
|
|
226
|
+
* previous (`git -C a -C b` runs in `a/b`). `--no-pager` and `-P` are
|
|
227
|
+
* accepted and mean nothing here, there is no pager. Any other leading
|
|
228
|
+
* option is refused: swallowing it would run the next word as a subcommand.
|
|
229
|
+
*/
|
|
230
|
+
export function parseGitGlobals(args, cwd) {
|
|
231
|
+
let dir = cwd;
|
|
232
|
+
let i = 0;
|
|
233
|
+
for (; i < args.length; i++) {
|
|
234
|
+
const arg = args[i];
|
|
235
|
+
if (arg === '-C') {
|
|
236
|
+
const path = args[++i];
|
|
237
|
+
if (path === undefined)
|
|
238
|
+
throw new Error("option '-C' requires a value");
|
|
239
|
+
dir = path.startsWith('/') ? path : dir + '/' + path;
|
|
240
|
+
dir = '/' + dir.split('/').filter((seg) => seg && seg !== '.').join('/');
|
|
241
|
+
}
|
|
242
|
+
else if (arg.startsWith('-C') && arg.length > 2) {
|
|
243
|
+
const path = arg.slice(2);
|
|
244
|
+
dir = path.startsWith('/') ? path : dir + '/' + path;
|
|
245
|
+
dir = '/' + dir.split('/').filter((seg) => seg && seg !== '.').join('/');
|
|
246
|
+
}
|
|
247
|
+
else if (arg === '--no-pager' || arg === '-P') {
|
|
248
|
+
// no pager to disable
|
|
249
|
+
}
|
|
250
|
+
else if (arg.startsWith('-') && arg !== '--version' && arg !== '-v' && arg !== '--help' && arg !== '-h') {
|
|
251
|
+
throw new Error(`unknown option '${arg}'\nusage: git [-C <path>] [--no-pager] <command> [<args>]`);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return { sub: args[i], subArgs: args.slice(i + 1), dir };
|
|
258
|
+
}
|
|
179
259
|
function getFlag(args, flag) {
|
|
180
260
|
const idx = args.indexOf(flag);
|
|
181
261
|
if (idx >= 0)
|
|
@@ -183,7 +263,7 @@ function getFlag(args, flag) {
|
|
|
183
263
|
const prefix = `${flag}=`;
|
|
184
264
|
return args.find((arg) => arg.startsWith(prefix))?.slice(prefix.length) || undefined;
|
|
185
265
|
}
|
|
186
|
-
export const CLONE_USAGE = 'usage: git clone [--depth <n>] [--no-shallow] [--branch <name> | -b <name>] [--bg] <url> [dir]';
|
|
266
|
+
export const CLONE_USAGE = 'usage: git clone [-q | --quiet] [--depth <n>] [--no-shallow] [--branch <name> | -b <name>] [--bg] <url> [dir]';
|
|
187
267
|
/**
|
|
188
268
|
* Every flag is either handled or refused loudly. Silently skipping unknown
|
|
189
269
|
* flags corrupted positionals for value-taking ones (`--branch dev URL`
|
|
@@ -195,6 +275,7 @@ export function parseCloneArgs(args) {
|
|
|
195
275
|
let branch;
|
|
196
276
|
let noShallow = false;
|
|
197
277
|
let isBg = false;
|
|
278
|
+
let quiet = false;
|
|
198
279
|
const positionals = [];
|
|
199
280
|
for (let i = 0; i < args.length; i++) {
|
|
200
281
|
const arg = args[i];
|
|
@@ -217,6 +298,10 @@ export function parseCloneArgs(args) {
|
|
|
217
298
|
noShallow = true;
|
|
218
299
|
else if (arg === '--bg' || arg === '&')
|
|
219
300
|
isBg = true;
|
|
301
|
+
else if (arg === '-q' || arg === '--quiet')
|
|
302
|
+
quiet = true;
|
|
303
|
+
// Progress is already the default; there is no more of it to ask for.
|
|
304
|
+
else if (arg === '-v' || arg === '--verbose') { /* accepted */ }
|
|
220
305
|
else if (name === '--filter') {
|
|
221
306
|
throw new Error("clone does not support '--filter': the bundled isomorphic-git has no " +
|
|
222
307
|
'partial-clone support, so a filter would silently download every object. ' +
|
|
@@ -236,13 +321,1579 @@ export function parseCloneArgs(args) {
|
|
|
236
321
|
noShallow,
|
|
237
322
|
isBg,
|
|
238
323
|
branch,
|
|
324
|
+
quiet,
|
|
239
325
|
};
|
|
240
326
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
327
|
+
/** fetch, pull and push: `-q`/`--quiet` wherever it appears; the other words keep their order. */
|
|
328
|
+
function takeQuiet(args) {
|
|
329
|
+
const rest = args.filter((arg) => arg !== '-q' && arg !== '--quiet');
|
|
330
|
+
return { quiet: rest.length !== args.length, rest };
|
|
331
|
+
}
|
|
332
|
+
/** commit's -m (repeatable), -q and -a, bundled as git allows (`-qm msg`, `-mmsg`); other options stay ignored. */
|
|
333
|
+
function parseCommitArgs(args) {
|
|
334
|
+
const messages = [];
|
|
335
|
+
let quiet = false;
|
|
336
|
+
let all = false;
|
|
337
|
+
let cleanup = null;
|
|
338
|
+
let allowEmptyMessage = false;
|
|
339
|
+
for (let i = 0; i < args.length; i++) {
|
|
340
|
+
const arg = args[i];
|
|
341
|
+
if (arg === '--')
|
|
342
|
+
break;
|
|
343
|
+
if (arg === '--quiet')
|
|
344
|
+
quiet = true;
|
|
345
|
+
else if (arg === '--all')
|
|
346
|
+
all = true;
|
|
347
|
+
else if (arg === '--allow-empty-message')
|
|
348
|
+
allowEmptyMessage = true;
|
|
349
|
+
else if (arg === '--cleanup')
|
|
350
|
+
cleanup = args[++i] ?? '';
|
|
351
|
+
else if (arg.startsWith('--cleanup='))
|
|
352
|
+
cleanup = arg.slice('--cleanup='.length);
|
|
353
|
+
else if (arg === '--message')
|
|
354
|
+
messages.push(args[++i] ?? '');
|
|
355
|
+
else if (arg.startsWith('--message='))
|
|
356
|
+
messages.push(arg.slice('--message='.length));
|
|
357
|
+
else if (/^-[^-]/.test(arg)) {
|
|
358
|
+
for (let j = 1; j < arg.length; j++) {
|
|
359
|
+
if (arg[j] === 'q')
|
|
360
|
+
quiet = true;
|
|
361
|
+
else if (arg[j] === 'a')
|
|
362
|
+
all = true;
|
|
363
|
+
else if (arg[j] === 'm') {
|
|
364
|
+
messages.push(j + 1 < arg.length ? arg.slice(j + 1) : args[++i] ?? '');
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return { messages, quiet, all, cleanup, allowEmptyMessage };
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* The message `-m` options make (builtin/commit.c opt_parse_m): each value
|
|
374
|
+
* ends in a newline, and one after another is joined by a blank line; an
|
|
375
|
+
* empty value adds nothing.
|
|
376
|
+
*/
|
|
377
|
+
function joinMessageOptions(values) {
|
|
378
|
+
let buf = '';
|
|
379
|
+
for (const value of values) {
|
|
380
|
+
if (buf.length)
|
|
381
|
+
buf += '\n';
|
|
382
|
+
buf += value;
|
|
383
|
+
if (buf.length && !buf.endsWith('\n'))
|
|
384
|
+
buf += '\n';
|
|
385
|
+
}
|
|
386
|
+
return buf;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* The cleanup mode a commit message gets (builtin/commit.c): --cleanup, else
|
|
390
|
+
* commit.cleanup. With no editor, as for -m, `default` and `scissors` are
|
|
391
|
+
* `whitespace`. Null for a mode git does not know.
|
|
392
|
+
*/
|
|
393
|
+
function commitCleanupMode(value) {
|
|
394
|
+
if (value === undefined || value === 'default' || value === 'scissors' || value === 'whitespace')
|
|
395
|
+
return 'whitespace';
|
|
396
|
+
if (value === 'strip' || value === 'verbatim')
|
|
397
|
+
return value;
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
function isNotFound(error) {
|
|
401
|
+
return error instanceof Error && 'code' in error && error.code === 'NotFoundError';
|
|
402
|
+
}
|
|
403
|
+
// ── Output ───────────────────────────────────────────────────────────────
|
|
404
|
+
/** Emit a binary string: its bytes verbatim where the sink keeps bytes, else as UTF-8 text. */
|
|
405
|
+
async function writeBinary(stream, bin) {
|
|
406
|
+
if (!bin)
|
|
407
|
+
return;
|
|
408
|
+
const bytes = bytesFromBinary(bin);
|
|
409
|
+
if (stream.writeBytes)
|
|
410
|
+
await stream.writeBytes(bytes);
|
|
411
|
+
else
|
|
412
|
+
await stream.write(dec.decode(bytes));
|
|
413
|
+
}
|
|
414
|
+
// ── Staging ──────────────────────────────────────────────────────────────
|
|
415
|
+
/**
|
|
416
|
+
* `commit -a`'s staging of tracked changes: the status refresh, the removals
|
|
417
|
+
* and the additions under one index write, as git's, and a path at a time
|
|
418
|
+
* (1,000 concurrent deflates reset the isolate).
|
|
419
|
+
*/
|
|
420
|
+
async function stageTracked(git, fs, dir) {
|
|
421
|
+
const cache = {};
|
|
422
|
+
const added = [];
|
|
423
|
+
const removed = [];
|
|
424
|
+
for (const [filepath, head, workdir, stage] of await git.statusMatrix({ fs, dir, cache, deferRefresh: true })) {
|
|
425
|
+
if (stage === 0)
|
|
426
|
+
continue;
|
|
427
|
+
if (head === workdir && workdir === stage)
|
|
428
|
+
continue;
|
|
429
|
+
if (workdir === 0)
|
|
430
|
+
removed.push(filepath);
|
|
431
|
+
else
|
|
432
|
+
added.push(filepath);
|
|
433
|
+
}
|
|
434
|
+
await git.stage({ fs, dir, cache, add: added, remove: removed, parallel: false });
|
|
435
|
+
}
|
|
436
|
+
// ── Repository discovery ─────────────────────────────────────────────────
|
|
437
|
+
const NOT_A_REPOSITORY = 'fatal: not a git repository (or any of the parent directories): .git\n';
|
|
438
|
+
const NOT_A_WORK_TREE = 'fatal: this operation must be run in a work tree\n';
|
|
439
|
+
/** setup_git_directory's walk: from the cwd up, a `.git` inside each level, else the level itself as a git directory. */
|
|
440
|
+
function discoverRepo(vfs, cwd) {
|
|
441
|
+
const isGitDir = (key) => {
|
|
442
|
+
const sub = (name) => (key ? `${key}/${name}` : name);
|
|
443
|
+
return vfs.isFile(sub('HEAD')) && vfs.isDirectory(sub('objects')) && vfs.isDirectory(sub('refs'));
|
|
444
|
+
};
|
|
445
|
+
const segments = normalizeVfsPath(cwd).split('/').filter(Boolean);
|
|
446
|
+
for (let depth = segments.length; depth >= 0; depth--) {
|
|
447
|
+
const dir = segments.slice(0, depth).join('/');
|
|
448
|
+
const prefix = segments.slice(depth).join('/');
|
|
449
|
+
const dotGit = dir ? `${dir}/.git` : '.git';
|
|
450
|
+
if (isGitDir(dotGit))
|
|
451
|
+
return { gitdir: `/${dotGit}`, worktree: `/${dir}`, prefix };
|
|
452
|
+
if (isGitDir(dir))
|
|
453
|
+
return { gitdir: `/${dir}`, worktree: null, prefix };
|
|
454
|
+
}
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
function ambiguousArgument(arg) {
|
|
458
|
+
return `fatal: ambiguous argument '${arg}': unknown revision or path not in the working tree.\n`
|
|
459
|
+
+ "Use '--' to separate paths from revisions, like this:\n"
|
|
460
|
+
+ "'git <command> [<revision>...] -- [<file>...]'\n";
|
|
461
|
+
}
|
|
462
|
+
/** A revision as rev-parse reads one: a ref, or a full or uniquely abbreviated object name. */
|
|
463
|
+
async function resolveRevision(git, fs, gitdir, rev, cache) {
|
|
464
|
+
const name = rev === '@' ? 'HEAD' : rev;
|
|
465
|
+
if (/[~^:{}\\]|\.\.|^@/.test(name))
|
|
466
|
+
throw new Error(`unsupported revision syntax '${rev}'`);
|
|
467
|
+
try {
|
|
468
|
+
return await git.resolveRef({ fs, gitdir, ref: name });
|
|
469
|
+
}
|
|
470
|
+
catch (e) {
|
|
471
|
+
if (!isNotFound(e))
|
|
472
|
+
throw e;
|
|
473
|
+
}
|
|
474
|
+
if (!/^[0-9a-f]{4,39}$/.test(name))
|
|
475
|
+
return null;
|
|
476
|
+
try {
|
|
477
|
+
return await git.expandOid({ fs, gitdir, oid: name, cache });
|
|
478
|
+
}
|
|
479
|
+
catch (e) {
|
|
480
|
+
if (!isNotFound(e))
|
|
481
|
+
throw e;
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/** --abbrev-ref: the short name of the ref a revision spells, null when it spells none. */
|
|
486
|
+
async function abbreviatedRef(git, fs, gitdir, rev) {
|
|
487
|
+
if (rev === 'HEAD' || rev === '@')
|
|
488
|
+
return (await git.currentBranch({ fs, gitdir })) ?? 'HEAD';
|
|
489
|
+
try {
|
|
490
|
+
const full = await git.expandRef({ fs, gitdir, ref: rev });
|
|
491
|
+
return full.replace(/^refs\/remotes\/(.+)\/HEAD$/, '$1').replace(/^refs\/(?:heads|tags|remotes)\//, '');
|
|
492
|
+
}
|
|
493
|
+
catch (e) {
|
|
494
|
+
if (!isNotFound(e))
|
|
495
|
+
throw e;
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
async function revParse(ctx, git, fs, vfs, args) {
|
|
500
|
+
const repo = discoverRepo(vfs, ctx.cwd);
|
|
501
|
+
if (!repo) {
|
|
502
|
+
await ctx.stderr.write(NOT_A_REPOSITORY);
|
|
503
|
+
return 128;
|
|
504
|
+
}
|
|
505
|
+
const cache = {};
|
|
506
|
+
let verify = false;
|
|
507
|
+
let quiet = false;
|
|
508
|
+
let abbrevRef = false;
|
|
509
|
+
let out = '';
|
|
510
|
+
const verified = [];
|
|
511
|
+
const show = async (rev, oid) => {
|
|
512
|
+
if (!abbrevRef)
|
|
513
|
+
return `${oid}\n`;
|
|
514
|
+
const name = await abbreviatedRef(git, fs, repo.gitdir, rev);
|
|
515
|
+
return name === null ? '' : `${name}\n`;
|
|
516
|
+
};
|
|
517
|
+
const fail = async (message, code) => {
|
|
518
|
+
if (out)
|
|
519
|
+
await ctx.stdout.write(out);
|
|
520
|
+
if (message)
|
|
521
|
+
await ctx.stderr.write(message);
|
|
522
|
+
return code;
|
|
523
|
+
};
|
|
524
|
+
const noSingleRevision = () => (quiet ? fail('', 1) : fail('fatal: Needed a single revision\n', 128));
|
|
525
|
+
for (const arg of args) {
|
|
526
|
+
switch (arg) {
|
|
527
|
+
case '--verify':
|
|
528
|
+
verify = true;
|
|
529
|
+
continue;
|
|
530
|
+
case '-q':
|
|
531
|
+
case '--quiet':
|
|
532
|
+
quiet = true;
|
|
533
|
+
continue;
|
|
534
|
+
case '--abbrev-ref':
|
|
535
|
+
abbrevRef = true;
|
|
536
|
+
continue;
|
|
537
|
+
case '--show-toplevel':
|
|
538
|
+
if (!repo.worktree)
|
|
539
|
+
return fail(NOT_A_WORK_TREE, 128);
|
|
540
|
+
out += `${repo.worktree}\n`;
|
|
541
|
+
continue;
|
|
542
|
+
case '--git-dir':
|
|
543
|
+
// rev-parse names the git directory relative to the cwd only from the top.
|
|
544
|
+
out += `${repo.prefix ? repo.gitdir : repo.worktree ? '.git' : '.'}\n`;
|
|
545
|
+
continue;
|
|
546
|
+
case '--is-inside-work-tree':
|
|
547
|
+
out += `${repo.worktree ? 'true' : 'false'}\n`;
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
if (arg.startsWith('-'))
|
|
551
|
+
return fail(`fatal: rev-parse: unsupported option '${arg}'\n`, 129);
|
|
552
|
+
const oid = await resolveRevision(git, fs, repo.gitdir, arg, cache);
|
|
553
|
+
if (oid === null) {
|
|
554
|
+
if (verify)
|
|
555
|
+
return noSingleRevision();
|
|
556
|
+
// A non-revision is echoed as a path, which must then exist.
|
|
557
|
+
out += `${arg}\n`;
|
|
558
|
+
if (vfs.exists(normalizeVfsPath(arg.startsWith('/') ? arg : `${ctx.cwd}/${arg}`)))
|
|
559
|
+
continue;
|
|
560
|
+
return fail(ambiguousArgument(arg), 128);
|
|
561
|
+
}
|
|
562
|
+
if (verify)
|
|
563
|
+
verified.push({ rev: arg, oid });
|
|
564
|
+
else
|
|
565
|
+
out += await show(arg, oid);
|
|
566
|
+
}
|
|
567
|
+
if (verify) {
|
|
568
|
+
if (verified.length !== 1)
|
|
569
|
+
return noSingleRevision();
|
|
570
|
+
out += await show(verified[0].rev, verified[0].oid);
|
|
571
|
+
}
|
|
572
|
+
await ctx.stdout.write(out);
|
|
573
|
+
return 0;
|
|
574
|
+
}
|
|
575
|
+
// ── Worktree inspection (ls-files, diff) ─────────────────────────────────
|
|
576
|
+
/** Whether git trusts the worktree's exec bit here: core.filemode, true unless set false. */
|
|
577
|
+
async function trustsFileMode(git, fs, dir) {
|
|
578
|
+
return (await git.getConfig({ fs, dir, path: 'core.filemode' })) !== false;
|
|
579
|
+
}
|
|
580
|
+
/** ce_mode_from_stat: without a trusted exec bit a file keeps its index mode, or 100644 when new. */
|
|
581
|
+
function worktreeMode(mode, indexMode, filemode) {
|
|
582
|
+
if (filemode || mode === 0o120000)
|
|
583
|
+
return mode;
|
|
584
|
+
return indexMode !== undefined && indexMode !== 0o120000 ? indexMode : 0o100644;
|
|
585
|
+
}
|
|
586
|
+
/** git's index and tree order: the UTF-8 bytes, which is code point order rather than UTF-16's. */
|
|
587
|
+
function comparePaths(a, b) {
|
|
588
|
+
for (let i = 0; i < a.length && i < b.length; i++) {
|
|
589
|
+
let x = a.charCodeAt(i);
|
|
590
|
+
let y = b.charCodeAt(i);
|
|
591
|
+
if (x === y)
|
|
592
|
+
continue;
|
|
593
|
+
// Surrogates (astral code points) sort above the rest of the BMP.
|
|
594
|
+
if (x >= 0xd800)
|
|
595
|
+
x = x >= 0xe000 ? x - 0x800 : x + 0x2000;
|
|
596
|
+
if (y >= 0xd800)
|
|
597
|
+
y = y >= 0xe000 ? y - 0x800 : y + 0x2000;
|
|
598
|
+
return x - y;
|
|
599
|
+
}
|
|
600
|
+
return a.length - b.length;
|
|
601
|
+
}
|
|
602
|
+
/** A repo-relative path as seen from `prefix`, climbing with '../' where it must. */
|
|
603
|
+
function relativeTo(path, prefix) {
|
|
604
|
+
if (!prefix)
|
|
605
|
+
return path;
|
|
606
|
+
if (path.startsWith(`${prefix}/`))
|
|
607
|
+
return path.slice(prefix.length + 1);
|
|
608
|
+
const from = prefix.split('/');
|
|
609
|
+
const to = path.split('/');
|
|
610
|
+
let shared = 0;
|
|
611
|
+
while (shared < from.length && shared < to.length - 1 && from[shared] === to[shared])
|
|
612
|
+
shared++;
|
|
613
|
+
return '../'.repeat(from.length - shared) + to.slice(shared).join('/');
|
|
614
|
+
}
|
|
615
|
+
/** Literal pathspecs, relative to the cwd, as repo-relative paths; '' is the whole tree. */
|
|
616
|
+
function repoPaths(args, cwd, worktree) {
|
|
617
|
+
const root = normalizeVfsPath(worktree);
|
|
618
|
+
return args.map((arg) => {
|
|
619
|
+
if (/[*?[]/.test(arg))
|
|
620
|
+
throw new Error(`pathspec '${arg}': globs are not supported, name the paths`);
|
|
621
|
+
const key = normalizeVfsPath(arg.startsWith('/') ? arg : `${cwd}/${arg}`);
|
|
622
|
+
if (!root || key === root)
|
|
623
|
+
return root ? '' : key;
|
|
624
|
+
if (key.startsWith(`${root}/`))
|
|
625
|
+
return key.slice(root.length + 1);
|
|
626
|
+
throw new Error(`${arg}: '${arg}' is outside repository at '${worktree}'`);
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
/** cf-git's walk one entry at a time, not every sibling at once; `visit` answers whether to descend. */
|
|
630
|
+
async function walkScoped(git, fs, dir, cache, trees, specs, visit) {
|
|
631
|
+
await git.walk({
|
|
632
|
+
fs, dir, cache, trees,
|
|
633
|
+
map: async (path, entries) => {
|
|
634
|
+
if (path === '.')
|
|
635
|
+
return true;
|
|
636
|
+
const inScope = specs.length === 0
|
|
637
|
+
|| specs.some((spec) => spec === '' || path === spec || path.startsWith(`${spec}/`));
|
|
638
|
+
// Outside the pathspecs a directory is entered only on the way to one.
|
|
639
|
+
if (!inScope)
|
|
640
|
+
return specs.some((spec) => spec.startsWith(`${path}/`)) ? true : null;
|
|
641
|
+
return (await visit(path, entries)) ? true : null;
|
|
642
|
+
},
|
|
643
|
+
reduce: async () => undefined,
|
|
644
|
+
iterate: async (walk, children) => {
|
|
645
|
+
for (const child of children)
|
|
646
|
+
await walk(child);
|
|
647
|
+
return [];
|
|
648
|
+
},
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
const ADD_USAGE = 'usage: git add [-n | --dry-run] [-v | --verbose] [-f | --force] [-A | --all | --no-all] '
|
|
652
|
+
+ '[-u | --update] [--] <pathspec>...\n';
|
|
653
|
+
/** git add's options this git does not do: they are git's, so they are refused as unsupported, not unknown. */
|
|
654
|
+
const ADD_UNSUPPORTED = new Set(['i', 'p', 'e', 'N', 'U', '--interactive', '--patch', '--edit', '--intent-to-add',
|
|
655
|
+
'--unified', '--inter-hunk-context', '--renormalize', '--refresh', '--ignore-errors', '--ignore-missing', '--sparse',
|
|
656
|
+
'--chmod', '--pathspec-from-file', '--pathspec-file-nul']);
|
|
657
|
+
/**
|
|
658
|
+
* `git add`: what git 2.x stages for its pathspecs. A pathspec takes the
|
|
659
|
+
* changes, deletions and new files below it (as -A does); -u only tracked
|
|
660
|
+
* paths, --no-all no deletions, -A or -u with no pathspec the whole tree.
|
|
661
|
+
* -n prints what would be staged (`add 'p'`, `remove 'p'`, tracked paths
|
|
662
|
+
* first, then new ones) and stages nothing; -v prints it and stages. A
|
|
663
|
+
* pathspec that matches nothing fails before anything is staged; one that
|
|
664
|
+
* names an ignored path is reported (exit 1) unless -f, the rest still added.
|
|
665
|
+
*/
|
|
666
|
+
async function addCommand(ctx, git, fs, vfs, args) {
|
|
667
|
+
let dryRun = false;
|
|
668
|
+
let verbose = false;
|
|
669
|
+
let force = false;
|
|
670
|
+
let all = null;
|
|
671
|
+
let update = false;
|
|
672
|
+
let dashdash = false;
|
|
673
|
+
const pathArgs = [];
|
|
674
|
+
for (const arg of args) {
|
|
675
|
+
if (dashdash || arg === '-' || !arg.startsWith('-')) {
|
|
676
|
+
pathArgs.push(arg);
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
if (arg === '--') {
|
|
680
|
+
dashdash = true;
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
const flags = arg.startsWith('--') ? [arg.split('=')[0]] : [...arg.slice(1)];
|
|
684
|
+
for (const flag of flags) {
|
|
685
|
+
switch (flag) {
|
|
686
|
+
case 'n':
|
|
687
|
+
case '--dry-run':
|
|
688
|
+
dryRun = true;
|
|
689
|
+
break;
|
|
690
|
+
case 'v':
|
|
691
|
+
case '--verbose':
|
|
692
|
+
verbose = true;
|
|
693
|
+
break;
|
|
694
|
+
case 'f':
|
|
695
|
+
case '--force':
|
|
696
|
+
force = true;
|
|
697
|
+
break;
|
|
698
|
+
case 'A':
|
|
699
|
+
case '--all':
|
|
700
|
+
case '--no-ignore-removal':
|
|
701
|
+
all = true;
|
|
702
|
+
break;
|
|
703
|
+
case '--no-all':
|
|
704
|
+
case '--ignore-removal':
|
|
705
|
+
all = false;
|
|
706
|
+
break;
|
|
707
|
+
case 'u':
|
|
708
|
+
case '--update':
|
|
709
|
+
update = true;
|
|
710
|
+
break;
|
|
711
|
+
default:
|
|
712
|
+
if (ADD_UNSUPPORTED.has(flag)) {
|
|
713
|
+
await ctx.stderr.write(`fatal: git add ${flag.length === 1 ? `-${flag}` : flag} is not supported here\n`);
|
|
714
|
+
return 128;
|
|
715
|
+
}
|
|
716
|
+
await ctx.stderr.write(flag.length === 1
|
|
717
|
+
? `error: unknown switch \`${flag}'\n${ADD_USAGE}`
|
|
718
|
+
: `error: unknown option \`${flag.slice(2)}'\n${ADD_USAGE}`);
|
|
719
|
+
return 129;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
const repo = discoverRepo(vfs, ctx.cwd);
|
|
724
|
+
if (!repo) {
|
|
725
|
+
await ctx.stderr.write(NOT_A_REPOSITORY);
|
|
726
|
+
return 128;
|
|
727
|
+
}
|
|
728
|
+
const root = repo.worktree;
|
|
729
|
+
if (!root) {
|
|
730
|
+
await ctx.stderr.write(NOT_A_WORK_TREE);
|
|
731
|
+
return 128;
|
|
732
|
+
}
|
|
733
|
+
if (pathArgs.length === 0 && all !== true && !update) {
|
|
734
|
+
await ctx.stderr.write("Nothing specified, nothing added.\nhint: Maybe you wanted to say 'git add .'?\n"
|
|
735
|
+
+ 'hint: Disable this message with "git config set advice.addEmptyPathspec false"\n');
|
|
736
|
+
return 0;
|
|
737
|
+
}
|
|
738
|
+
const specs = pathArgs.length ? repoPaths(pathArgs, ctx.cwd, root) : [''];
|
|
739
|
+
const inSpec = (path, spec) => spec === '' || path === spec || path.startsWith(`${spec}/`);
|
|
740
|
+
const cache = {};
|
|
741
|
+
// Its stat refreshes ride in the one index write that stages (git add writes once).
|
|
742
|
+
const matrix = await git.statusMatrix({ fs, dir: root, cache, deferRefresh: !dryRun });
|
|
743
|
+
// -f takes the ignored files below each pathspec as well.
|
|
744
|
+
const forced = force
|
|
745
|
+
? await git.statusMatrix({ fs, dir: root, cache, ignored: true, filepaths: specs.map((spec) => spec || '.') })
|
|
746
|
+
: [];
|
|
747
|
+
const rows = new Map();
|
|
748
|
+
for (const [path, head, workdir, stage] of [...matrix, ...forced]) {
|
|
749
|
+
// -u updates what the index holds and nothing else: a path it does not
|
|
750
|
+
// hold (never added, or removed with rm --cached) is not git's to update.
|
|
751
|
+
if (update && stage === 0)
|
|
752
|
+
continue;
|
|
753
|
+
if (specs.some((spec) => inSpec(path, spec)))
|
|
754
|
+
rows.set(path, [head, workdir, stage]);
|
|
755
|
+
}
|
|
756
|
+
const ignored = [];
|
|
757
|
+
for (const [i, spec] of specs.entries()) {
|
|
758
|
+
if (spec === '' || [...rows.keys()].some((path) => inSpec(path, spec)))
|
|
759
|
+
continue;
|
|
760
|
+
if (update) {
|
|
761
|
+
// "Known to git" is the index; git names the first pathspec it holds nothing under.
|
|
762
|
+
await ctx.stderr.write(`error: pathspec '${pathArgs[i]}' did not match any file(s) known to git\n`);
|
|
763
|
+
return 128;
|
|
764
|
+
}
|
|
765
|
+
let st = null;
|
|
766
|
+
try {
|
|
767
|
+
st = vfs.lstat(normalizeVfsPath(`${root}/${spec}`));
|
|
768
|
+
}
|
|
769
|
+
catch { /* absent */ }
|
|
770
|
+
if (!st) {
|
|
771
|
+
await ctx.stderr.write(`fatal: pathspec '${pathArgs[i]}' did not match any files\n`);
|
|
772
|
+
return 128;
|
|
773
|
+
}
|
|
774
|
+
// Named but not listed: ignored (reported at the path the ignore rule names), or an empty directory.
|
|
775
|
+
const parts = spec.split('/');
|
|
776
|
+
for (let depth = 1; depth <= parts.length; depth++) {
|
|
777
|
+
const prefix = parts.slice(0, depth).join('/');
|
|
778
|
+
if (await git.isIgnored({ fs, dir: root, filepath: prefix })) {
|
|
779
|
+
if (!ignored.includes(prefix))
|
|
780
|
+
ignored.push(prefix);
|
|
781
|
+
break;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
const tracked = [];
|
|
786
|
+
const untracked = [];
|
|
787
|
+
for (const [path, [head, workdir, stage]] of [...rows].sort(([a], [b]) => comparePaths(a, b))) {
|
|
788
|
+
if (head === 0 && stage === 0) {
|
|
789
|
+
if (workdir !== 0 && !update)
|
|
790
|
+
untracked.push(path);
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
if (workdir === 0) {
|
|
794
|
+
if (stage !== 0 && all !== false)
|
|
795
|
+
tracked.push([path, 'remove']);
|
|
796
|
+
}
|
|
797
|
+
else if (stage !== workdir) {
|
|
798
|
+
tracked.push([path, 'add']);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (ignored.length) {
|
|
802
|
+
await ctx.stderr.write(`The following paths are ignored by one of your .gitignore files:\n${ignored.map((p) => `${p}\n`).join('')}`
|
|
803
|
+
+ 'hint: Use -f if you really want to add them.\n'
|
|
804
|
+
+ 'hint: Disable this message with "git config set advice.addIgnoredFile false"\n');
|
|
805
|
+
}
|
|
806
|
+
let out = '';
|
|
807
|
+
for (const [path, action] of tracked)
|
|
808
|
+
out += `${action} '${path}'\n`;
|
|
809
|
+
for (const path of untracked)
|
|
810
|
+
out += `add '${path}'\n`;
|
|
811
|
+
if (dryRun || verbose)
|
|
812
|
+
await writeBinary(ctx.stdout, out);
|
|
813
|
+
if (!dryRun) {
|
|
814
|
+
const removed = tracked.filter(([, action]) => action === 'remove').map(([path]) => path);
|
|
815
|
+
const added = [...tracked.filter(([, action]) => action === 'add').map(([path]) => path), ...untracked];
|
|
816
|
+
await git.stage({ fs, dir: root, cache, add: added, remove: removed, parallel: false, force: true });
|
|
817
|
+
}
|
|
818
|
+
return ignored.length ? 1 : 0;
|
|
819
|
+
}
|
|
820
|
+
const TAG_USAGE = 'usage: git tag [-a] [-f] [-m <msg> | -F <file>] <tagname> [<commit>]\n'
|
|
821
|
+
+ ' or: git tag -d <tagname>...\n'
|
|
822
|
+
+ ' or: git tag [-n[<num>]] [-l] [<pattern>...]\n';
|
|
823
|
+
/** git's `whitespace` cleanup of a message given with -m or -F: no trailing blanks, no runs or edges of empty lines. */
|
|
824
|
+
/** A `±hhmm` timezone for an offset in minutes east of UTC. */
|
|
825
|
+
function gitTimezone(minutesEast) {
|
|
826
|
+
const sign = minutesEast < 0 ? '-' : '+';
|
|
827
|
+
const abs = Math.abs(minutesEast);
|
|
828
|
+
return `${sign}${String(Math.floor(abs / 60)).padStart(2, '0')}${String(abs % 60).padStart(2, '0')}`;
|
|
829
|
+
}
|
|
830
|
+
// git's date.c tables, in its order: a name matches from its third letter on.
|
|
831
|
+
const GIT_MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
|
832
|
+
const GIT_WEEKDAYS = ['Sundays', 'Mondays', 'Tuesdays', 'Wednesdays', 'Thursdays', 'Fridays', 'Saturdays'];
|
|
833
|
+
/** [name, hours east, daylight]: git adds the daylight hour ("This is bogus, but we like summer"). */
|
|
834
|
+
const GIT_ZONES = [
|
|
835
|
+
['IDLW', -12, 0], ['NT', -11, 0], ['CAT', -10, 0], ['HST', -10, 0], ['HDT', -10, 1], ['YST', -9, 0], ['YDT', -9, 1],
|
|
836
|
+
['PST', -8, 0], ['PDT', -8, 1], ['MST', -7, 0], ['MDT', -7, 1], ['CST', -6, 0], ['CDT', -6, 1], ['EST', -5, 0],
|
|
837
|
+
['EDT', -5, 1], ['AST', -3, 0], ['ADT', -3, 1], ['WAT', -1, 0], ['GMT', 0, 0], ['UTC', 0, 0], ['Z', 0, 0],
|
|
838
|
+
['WET', 0, 0], ['BST', 0, 1], ['CET', 1, 0], ['MET', 1, 0], ['MEWT', 1, 0], ['MEST', 1, 1], ['CEST', 1, 1],
|
|
839
|
+
['MESZ', 1, 1], ['FWT', 1, 0], ['FST', 1, 1], ['EET', 2, 0], ['EEST', 2, 1], ['WAST', 7, 0], ['WADT', 7, 1],
|
|
840
|
+
['CCT', 8, 0], ['JST', 9, 0], ['EAST', 10, 0], ['EADT', 10, 1], ['GST', 10, 0], ['NZT', 12, 0], ['NZST', 12, 0],
|
|
841
|
+
['NZDT', 12, 1], ['IDLE', 12, 0],
|
|
842
|
+
];
|
|
843
|
+
const isDigit = (c) => c !== undefined && c >= '0' && c <= '9';
|
|
844
|
+
const isAlpha = (c) => c !== undefined && /[A-Za-z]/.test(c);
|
|
845
|
+
const isAlnum = (c) => isDigit(c) || isAlpha(c);
|
|
846
|
+
/**
|
|
847
|
+
* A GIT_COMMITTER_DATE as git's date.c parse_date_basic() reads it, ported
|
|
848
|
+
* routine for routine (match_object_header_date, match_alpha, match_digit,
|
|
849
|
+
* match_multi_number, set_date, match_tz, tm_to_time_t), so every form git
|
|
850
|
+
* accepts is accepted and read the same way: `@<secs> ±hhmm` (git's own raw
|
|
851
|
+
* form), a bare epoch of 9 or more digits, ISO 8601, RFC 2822 (`date -R`),
|
|
852
|
+
* git's default `Thu Jan 2 03:04:05 2020 -0800`, dotted and slashed dates,
|
|
853
|
+
* zone names, am/pm. Without a zone, the local one at that time, as mktime()
|
|
854
|
+
* gives it. Null where git says "invalid date format": no time of day, a
|
|
855
|
+
* year outside 1970-2099, a date it cannot place.
|
|
856
|
+
*/
|
|
857
|
+
function parseGitDate(text, nowSeconds = Math.floor(Date.now() / 1000)) {
|
|
858
|
+
// match_object_header_date: "@<digits> ±hhmm", exactly.
|
|
859
|
+
const header = /^@(\d+) ([+-])(\d{4})(?:\n|$)/.exec(text);
|
|
860
|
+
if (header) {
|
|
861
|
+
const hhmm = Number(header[3]);
|
|
862
|
+
const minutes = (header[2] === '-' ? -1 : 1) * (Math.floor(hhmm / 100) * 60 + (hhmm % 100));
|
|
863
|
+
return { seconds: Number(header[1]), zone: gitTimezone(minutes) };
|
|
864
|
+
}
|
|
865
|
+
const tm = { year: -1, mon: -1, mday: -1, hour: -1, min: -1, sec: -1, wday: -1 };
|
|
866
|
+
let offset = null;
|
|
867
|
+
let gmt = false;
|
|
868
|
+
// tm_to_time_t: 1970-2099 only, and every field of the time of day given.
|
|
869
|
+
const toTime = (t) => {
|
|
870
|
+
const days = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
|
|
871
|
+
const year = t.year - 70;
|
|
872
|
+
let day = t.mday;
|
|
873
|
+
if (year < 0 || year > 129 || t.mon < 0 || t.mon > 11)
|
|
874
|
+
return null;
|
|
875
|
+
if (t.mon < 2 || (year + 2) % 4)
|
|
876
|
+
day--;
|
|
877
|
+
if (t.hour < 0 || t.min < 0 || t.sec < 0)
|
|
878
|
+
return null;
|
|
879
|
+
return (year * 365 + Math.floor((year + 1) / 4) + days[t.mon] + day) * 86_400 + t.hour * 3600 + t.min * 60 + t.sec;
|
|
245
880
|
};
|
|
881
|
+
const noDate = () => tm.year < 0 && tm.mon < 0 && tm.mday < 0 && tm.hour < 0 && tm.min < 0 && tm.sec < 0;
|
|
882
|
+
const matchString = (at, word) => {
|
|
883
|
+
let i = 0;
|
|
884
|
+
for (; at + i < text.length; i++) {
|
|
885
|
+
const c = text[at + i];
|
|
886
|
+
if (c === word[i] || c.toUpperCase() === (word[i] ?? '').toUpperCase())
|
|
887
|
+
continue;
|
|
888
|
+
if (!isAlnum(c))
|
|
889
|
+
break;
|
|
890
|
+
return 0;
|
|
891
|
+
}
|
|
892
|
+
return i;
|
|
893
|
+
};
|
|
894
|
+
// set_date: month and day in range, the year placed as git places it, and
|
|
895
|
+
// (for a guessed order) no more than ten days into the future.
|
|
896
|
+
const setDate = (year, month, day, refuseFuture) => {
|
|
897
|
+
if (!(month > 0 && month < 13 && day > 0 && day < 32))
|
|
898
|
+
return false;
|
|
899
|
+
const r = { ...tm, mon: month - 1, mday: day };
|
|
900
|
+
if (year === -1) {
|
|
901
|
+
if (!refuseFuture)
|
|
902
|
+
return false;
|
|
903
|
+
r.year = new Date(nowSeconds * 1000).getUTCFullYear() - 1900;
|
|
904
|
+
}
|
|
905
|
+
else if (year >= 1970 && year < 2100)
|
|
906
|
+
r.year = year - 1900;
|
|
907
|
+
else if (year > 70 && year < 100)
|
|
908
|
+
r.year = year;
|
|
909
|
+
else if (year < 38)
|
|
910
|
+
r.year = year + 100;
|
|
911
|
+
else
|
|
912
|
+
return false;
|
|
913
|
+
if (refuseFuture) {
|
|
914
|
+
const specified = toTime({ ...r, hour: 0, min: 0, sec: 0 });
|
|
915
|
+
if (specified !== null && nowSeconds + 10 * 86_400 < specified)
|
|
916
|
+
return false;
|
|
917
|
+
}
|
|
918
|
+
tm.mon = r.mon;
|
|
919
|
+
tm.mday = r.mday;
|
|
920
|
+
if (year !== -1 || !refuseFuture)
|
|
921
|
+
tm.year = r.year;
|
|
922
|
+
return true;
|
|
923
|
+
};
|
|
924
|
+
const readNumber = (at) => {
|
|
925
|
+
let end = at;
|
|
926
|
+
while (isDigit(text[end]))
|
|
927
|
+
end++;
|
|
928
|
+
return [Number(text.slice(at, end)), end];
|
|
929
|
+
};
|
|
930
|
+
const matchMulti = (num, sep, at, end) => {
|
|
931
|
+
let num3 = -1;
|
|
932
|
+
let [num2, next] = readNumber(end + 1);
|
|
933
|
+
if (text[next] === sep && isDigit(text[next + 1]))
|
|
934
|
+
[num3, next] = readNumber(next + 1);
|
|
935
|
+
if (sep === ':') {
|
|
936
|
+
if (num3 < 0)
|
|
937
|
+
num3 = 0;
|
|
938
|
+
if (num < 25 && num2 >= 0 && num2 < 60 && num3 >= 0 && num3 <= 60) {
|
|
939
|
+
tm.hour = num;
|
|
940
|
+
tm.min = num2;
|
|
941
|
+
tm.sec = num3;
|
|
942
|
+
return next - at;
|
|
943
|
+
}
|
|
944
|
+
return 0;
|
|
945
|
+
}
|
|
946
|
+
if (num > 70 && (setDate(num, num2, num3, false) || setDate(num, num3, num2, false)))
|
|
947
|
+
return next - at;
|
|
948
|
+
if (sep !== '.' && setDate(num3, num, num2, true))
|
|
949
|
+
return next - at;
|
|
950
|
+
if (setDate(num3, num2, num, true))
|
|
951
|
+
return next - at;
|
|
952
|
+
if (sep === '.' && setDate(num3, num, num2, true))
|
|
953
|
+
return next - at;
|
|
954
|
+
return 0;
|
|
955
|
+
};
|
|
956
|
+
const matchDigit = (at) => {
|
|
957
|
+
const [num, end] = readNumber(at);
|
|
958
|
+
// Seconds since 1970: a number of 9 digits or more, before any date.
|
|
959
|
+
if (num >= 100_000_000 && noDate()) {
|
|
960
|
+
const d = new Date(num * 1000);
|
|
961
|
+
Object.assign(tm, { year: d.getUTCFullYear() - 1900, mon: d.getUTCMonth(), mday: d.getUTCDate(), hour: d.getUTCHours(), min: d.getUTCMinutes(), sec: d.getUTCSeconds() });
|
|
962
|
+
gmt = true;
|
|
963
|
+
return end - at;
|
|
964
|
+
}
|
|
965
|
+
if ((text[end] === ':' || text[end] === '.' || text[end] === '/' || text[end] === '-') && isDigit(text[end + 1])) {
|
|
966
|
+
const matched = matchMulti(num, text[end], at, end);
|
|
967
|
+
if (matched)
|
|
968
|
+
return matched;
|
|
969
|
+
}
|
|
970
|
+
const n = end - at;
|
|
971
|
+
if (n === 8 || n === 6) {
|
|
972
|
+
const [a, b, c] = [Math.floor(num / 10000), Math.floor((num % 10000) / 100), num % 100];
|
|
973
|
+
if (n === 8)
|
|
974
|
+
setDate(a, b, c, false);
|
|
975
|
+
else if (a < 25 && b < 60 && c <= 60) {
|
|
976
|
+
tm.hour = a;
|
|
977
|
+
tm.min = b;
|
|
978
|
+
tm.sec = c;
|
|
979
|
+
if (text[end] === '.' && isDigit(text[end + 1]))
|
|
980
|
+
return readNumber(end + 1)[1] - at;
|
|
981
|
+
}
|
|
982
|
+
return n;
|
|
983
|
+
}
|
|
984
|
+
if (n === 4) {
|
|
985
|
+
if (num <= 1400 && offset === null)
|
|
986
|
+
offset = Math.floor(num / 100) * 60 + (num % 100);
|
|
987
|
+
else if (num > 1900 && num < 2100)
|
|
988
|
+
tm.year = num - 1900;
|
|
989
|
+
return n;
|
|
990
|
+
}
|
|
991
|
+
if (n > 2)
|
|
992
|
+
return n;
|
|
993
|
+
if (num > 0 && num < 32 && tm.mday < 0) {
|
|
994
|
+
tm.mday = num;
|
|
995
|
+
return n;
|
|
996
|
+
}
|
|
997
|
+
if (n === 2 && tm.year < 0) {
|
|
998
|
+
if (num < 10 && tm.mday >= 0) {
|
|
999
|
+
tm.year = num + 100;
|
|
1000
|
+
return n;
|
|
1001
|
+
}
|
|
1002
|
+
if (num >= 70) {
|
|
1003
|
+
tm.year = num;
|
|
1004
|
+
return n;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
if (num > 0 && num < 13 && tm.mon < 0)
|
|
1008
|
+
tm.mon = num - 1;
|
|
1009
|
+
return n;
|
|
1010
|
+
};
|
|
1011
|
+
const matchAlpha = (at) => {
|
|
1012
|
+
for (let i = 0; i < 12; i++) {
|
|
1013
|
+
const m = matchString(at, GIT_MONTHS[i]);
|
|
1014
|
+
if (m >= 3) {
|
|
1015
|
+
tm.mon = i;
|
|
1016
|
+
return m;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
for (let i = 0; i < 7; i++) {
|
|
1020
|
+
const m = matchString(at, GIT_WEEKDAYS[i]);
|
|
1021
|
+
if (m >= 3) {
|
|
1022
|
+
tm.wday = i;
|
|
1023
|
+
return m;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
for (const [name, hours, dst] of GIT_ZONES) {
|
|
1027
|
+
const m = matchString(at, name);
|
|
1028
|
+
if (m >= 3 || m === name.length) {
|
|
1029
|
+
if (offset === null)
|
|
1030
|
+
offset = 60 * (hours + dst);
|
|
1031
|
+
return m;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
if (matchString(at, 'PM') === 2) {
|
|
1035
|
+
tm.hour = (tm.hour % 12) + 12;
|
|
1036
|
+
return 2;
|
|
1037
|
+
}
|
|
1038
|
+
if (matchString(at, 'AM') === 2) {
|
|
1039
|
+
tm.hour = tm.hour % 12;
|
|
1040
|
+
return 2;
|
|
1041
|
+
}
|
|
1042
|
+
// ISO 8601's 'T' before a time.
|
|
1043
|
+
if (text[at] === 'T' && isDigit(text[at + 1]) && tm.hour === -1) {
|
|
1044
|
+
tm.min = 0;
|
|
1045
|
+
tm.sec = 0;
|
|
1046
|
+
return 1;
|
|
1047
|
+
}
|
|
1048
|
+
let skip = 0;
|
|
1049
|
+
while (isAlpha(text[at + skip]))
|
|
1050
|
+
skip++;
|
|
1051
|
+
return skip;
|
|
1052
|
+
};
|
|
1053
|
+
const matchTz = (at) => {
|
|
1054
|
+
let [hour, end] = readNumber(at + 1);
|
|
1055
|
+
const n = end - (at + 1);
|
|
1056
|
+
let min = 0;
|
|
1057
|
+
if (n === 4) {
|
|
1058
|
+
min = hour % 100;
|
|
1059
|
+
hour = Math.floor(hour / 100);
|
|
1060
|
+
}
|
|
1061
|
+
else if (n !== 2)
|
|
1062
|
+
min = 99;
|
|
1063
|
+
else if (text[end] === ':') {
|
|
1064
|
+
[min, end] = readNumber(end + 1);
|
|
1065
|
+
if (end - (at + 1) !== 5)
|
|
1066
|
+
min = 99;
|
|
1067
|
+
}
|
|
1068
|
+
if (min < 60 && hour < 24)
|
|
1069
|
+
offset = (text[at] === '-' ? -1 : 1) * (hour * 60 + min);
|
|
1070
|
+
return end - at;
|
|
1071
|
+
};
|
|
1072
|
+
for (let at = 0; at < text.length && text[at] !== '\n';) {
|
|
1073
|
+
const c = text[at];
|
|
1074
|
+
let matched = 0;
|
|
1075
|
+
if (isAlpha(c))
|
|
1076
|
+
matched = matchAlpha(at);
|
|
1077
|
+
else if (isDigit(c))
|
|
1078
|
+
matched = matchDigit(at);
|
|
1079
|
+
else if ((c === '-' || c === '+') && isDigit(text[at + 1]))
|
|
1080
|
+
matched = matchTz(at);
|
|
1081
|
+
at += matched || 1;
|
|
1082
|
+
}
|
|
1083
|
+
let seconds = toTime(tm);
|
|
1084
|
+
if (seconds === null)
|
|
1085
|
+
return null;
|
|
1086
|
+
let minutesEast;
|
|
1087
|
+
if (offset === null) {
|
|
1088
|
+
// No zone given: the local one at that time, as mktime() reads the same fields.
|
|
1089
|
+
const local = new Date(tm.year + 1900, tm.mon, tm.mday, tm.hour, tm.min, tm.sec).getTime() / 1000;
|
|
1090
|
+
minutesEast = Math.round((seconds - local) / 60);
|
|
1091
|
+
}
|
|
1092
|
+
else
|
|
1093
|
+
minutesEast = offset;
|
|
1094
|
+
if (!gmt)
|
|
1095
|
+
seconds -= minutesEast * 60;
|
|
1096
|
+
return { seconds, zone: gitTimezone(minutesEast) };
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* The author or committer identity git stamps an object with (ident.c):
|
|
1100
|
+
* GIT_<ROLE>_NAME and GIT_<ROLE>_EMAIL, else user.name and user.email from
|
|
1101
|
+
* the config, then the login name; and GIT_<ROLE>_DATE read by
|
|
1102
|
+
* parseGitDate, or now. A date git cannot read is its error.
|
|
1103
|
+
*/
|
|
1104
|
+
async function gitIdent(ctx, git, fs, dir, role) {
|
|
1105
|
+
const config = async (key) => {
|
|
1106
|
+
try {
|
|
1107
|
+
const value = await git.getConfig({ fs, dir, path: key });
|
|
1108
|
+
return typeof value === 'string' ? value : undefined;
|
|
1109
|
+
}
|
|
1110
|
+
catch {
|
|
1111
|
+
return undefined;
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
const name = ctx.env[`GIT_${role}_NAME`] || await config('user.name') || ctx.env.USER || 'user';
|
|
1115
|
+
const email = ctx.env[`GIT_${role}_EMAIL`] || await config('user.email') || 'user@nimbus.dev';
|
|
1116
|
+
const raw = ctx.env[`GIT_${role}_DATE`];
|
|
1117
|
+
let seconds;
|
|
1118
|
+
let minutesEast;
|
|
1119
|
+
if (raw) {
|
|
1120
|
+
const parsed = parseGitDate(raw);
|
|
1121
|
+
if (!parsed)
|
|
1122
|
+
return { error: `fatal: invalid date format: ${raw}\n` };
|
|
1123
|
+
seconds = parsed.seconds;
|
|
1124
|
+
const zone = /^([+-])(\d{2})(\d{2})$/.exec(parsed.zone);
|
|
1125
|
+
minutesEast = (zone[1] === '-' ? -1 : 1) * (Number(zone[2]) * 60 + Number(zone[3]));
|
|
1126
|
+
}
|
|
1127
|
+
else {
|
|
1128
|
+
seconds = Math.floor(Date.now() / 1000);
|
|
1129
|
+
minutesEast = -new Date(seconds * 1000).getTimezoneOffset();
|
|
1130
|
+
}
|
|
1131
|
+
// Not -0: cf-git writes a negative zero offset as -0000, which is git's "zone unknown".
|
|
1132
|
+
return { name, email, timestamp: seconds, timezoneOffset: minutesEast === 0 ? 0 : -minutesEast };
|
|
1133
|
+
}
|
|
1134
|
+
/** An identity as a signature line: `Name <email> <seconds> ±hhmm`. */
|
|
1135
|
+
function identLine(ident) {
|
|
1136
|
+
return `${ident.name} <${ident.email}> ${ident.timestamp} ${gitTimezone(-ident.timezoneOffset)}`;
|
|
1137
|
+
}
|
|
1138
|
+
/** Both identities a commit takes, or git's error for a date it cannot read. */
|
|
1139
|
+
async function commitIdents(ctx, git, fs, dir) {
|
|
1140
|
+
const author = await gitIdent(ctx, git, fs, dir, 'AUTHOR');
|
|
1141
|
+
if ('error' in author)
|
|
1142
|
+
return author;
|
|
1143
|
+
const committer = await gitIdent(ctx, git, fs, dir, 'COMMITTER');
|
|
1144
|
+
if ('error' in committer)
|
|
1145
|
+
return committer;
|
|
1146
|
+
return { author, committer };
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* git's stripspace (strbuf_stripspace): trailing whitespace, runs of blank
|
|
1150
|
+
* lines and blank lines at either end go, and with `stripComments` (the
|
|
1151
|
+
* `strip` cleanup, the default for a tag message) lines starting with the
|
|
1152
|
+
* comment character too. A non-empty result ends with a newline.
|
|
1153
|
+
*/
|
|
1154
|
+
function cleanupMessage(text, stripComments = true) {
|
|
1155
|
+
const lines = text.split('\n').filter((line) => !stripComments || !line.startsWith('#')).map((line) => line.replace(/\s+$/, ''));
|
|
1156
|
+
const out = [];
|
|
1157
|
+
for (const line of lines) {
|
|
1158
|
+
if (line === '' && (out.length === 0 || out[out.length - 1] === ''))
|
|
1159
|
+
continue;
|
|
1160
|
+
out.push(line);
|
|
1161
|
+
}
|
|
1162
|
+
while (out.length > 0 && out[out.length - 1] === '')
|
|
1163
|
+
out.pop();
|
|
1164
|
+
return out.length ? `${out.join('\n')}\n` : '';
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Commit the index with exactly `message`, git's cleanup already applied:
|
|
1168
|
+
* one object, written byte for byte (the tracked cf-git patch's rawMessage;
|
|
1169
|
+
* cf-git's own rendering would normalize the newlines of a verbatim message
|
|
1170
|
+
* and refuse an empty one), and the branch, or a detached HEAD, moved by
|
|
1171
|
+
* cf-git.
|
|
1172
|
+
*/
|
|
1173
|
+
async function writeCommit(git, fs, dir, message, idents) {
|
|
1174
|
+
return git.commit({ fs, dir, message, ...idents, rawMessage: true });
|
|
1175
|
+
}
|
|
1176
|
+
/** A tag pattern (fnmatch: `*`, `?`, `[...]`) as a whole-name regular expression. */
|
|
1177
|
+
function tagPattern(pattern) {
|
|
1178
|
+
let source = '';
|
|
1179
|
+
for (const ch of pattern) {
|
|
1180
|
+
source += ch === '*' ? '.*' : ch === '?' ? '.' : ch === '[' || ch === ']' ? ch : ch.replace(/[.*+?^${}()|\\/]/g, '\\$&');
|
|
1181
|
+
}
|
|
1182
|
+
return new RegExp(`^${source}$`);
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* `git tag`: list (-l, patterns, -n<num> with each tag's message or its
|
|
1186
|
+
* commit's subject), create (lightweight, or annotated with -a, -m or -F,
|
|
1187
|
+
* a tag object as git writes it), replace (-f) and delete (-d), with git's
|
|
1188
|
+
* messages and exit codes.
|
|
1189
|
+
*/
|
|
1190
|
+
async function tagCommand(ctx, git, fs, vfs, args) {
|
|
1191
|
+
let annotate = false;
|
|
1192
|
+
let force = false;
|
|
1193
|
+
let del = false;
|
|
1194
|
+
let list = false;
|
|
1195
|
+
let lines = null;
|
|
1196
|
+
const messages = [];
|
|
1197
|
+
let messageFile = null;
|
|
1198
|
+
const operands = [];
|
|
1199
|
+
for (let i = 0; i < args.length; i++) {
|
|
1200
|
+
const arg = args[i];
|
|
1201
|
+
if (arg === '--') {
|
|
1202
|
+
operands.push(...args.slice(i + 1));
|
|
1203
|
+
break;
|
|
1204
|
+
}
|
|
1205
|
+
if (arg === '-' || !arg.startsWith('-')) {
|
|
1206
|
+
operands.push(arg);
|
|
1207
|
+
continue;
|
|
1208
|
+
}
|
|
1209
|
+
if (arg === '--annotate') {
|
|
1210
|
+
annotate = true;
|
|
1211
|
+
continue;
|
|
1212
|
+
}
|
|
1213
|
+
if (arg === '--force') {
|
|
1214
|
+
force = true;
|
|
1215
|
+
continue;
|
|
1216
|
+
}
|
|
1217
|
+
if (arg === '--delete') {
|
|
1218
|
+
del = true;
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
1221
|
+
if (arg === '--list') {
|
|
1222
|
+
list = true;
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
1225
|
+
if (arg.startsWith('--message=')) {
|
|
1226
|
+
messages.push(arg.slice('--message='.length));
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
if (arg === '--message') {
|
|
1230
|
+
messages.push(args[++i] ?? '');
|
|
1231
|
+
continue;
|
|
1232
|
+
}
|
|
1233
|
+
if (arg.startsWith('--file=')) {
|
|
1234
|
+
messageFile = arg.slice('--file='.length);
|
|
1235
|
+
continue;
|
|
1236
|
+
}
|
|
1237
|
+
if (arg === '--file') {
|
|
1238
|
+
messageFile = args[++i] ?? '';
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
if (arg.startsWith('--')) {
|
|
1242
|
+
await ctx.stderr.write(`error: unknown option \`${arg.slice(2)}'\n${TAG_USAGE}`);
|
|
1243
|
+
return 129;
|
|
1244
|
+
}
|
|
1245
|
+
for (let j = 1; j < arg.length; j++) {
|
|
1246
|
+
const flag = arg[j];
|
|
1247
|
+
if (flag === 'a')
|
|
1248
|
+
annotate = true;
|
|
1249
|
+
else if (flag === 'f')
|
|
1250
|
+
force = true;
|
|
1251
|
+
else if (flag === 'd')
|
|
1252
|
+
del = true;
|
|
1253
|
+
else if (flag === 'l')
|
|
1254
|
+
list = true;
|
|
1255
|
+
else if (flag === 'n') {
|
|
1256
|
+
const digits = /^\d*/.exec(arg.slice(j + 1))?.[0] ?? '';
|
|
1257
|
+
lines = digits ? Number(digits) : 1;
|
|
1258
|
+
j += digits.length;
|
|
1259
|
+
}
|
|
1260
|
+
else if (flag === 'm' || flag === 'F') {
|
|
1261
|
+
const value = arg.slice(j + 1) || args[++i];
|
|
1262
|
+
if (value === undefined) {
|
|
1263
|
+
await ctx.stderr.write(`error: switch \`${flag}' requires a value\n${TAG_USAGE}`);
|
|
1264
|
+
return 129;
|
|
1265
|
+
}
|
|
1266
|
+
if (flag === 'm')
|
|
1267
|
+
messages.push(value);
|
|
1268
|
+
else
|
|
1269
|
+
messageFile = value;
|
|
1270
|
+
break;
|
|
1271
|
+
}
|
|
1272
|
+
else {
|
|
1273
|
+
await ctx.stderr.write(`error: unknown switch \`${flag}'\n${TAG_USAGE}`);
|
|
1274
|
+
return 129;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
const repo = discoverRepo(vfs, ctx.cwd);
|
|
1279
|
+
if (!repo) {
|
|
1280
|
+
await ctx.stderr.write(NOT_A_REPOSITORY);
|
|
1281
|
+
return 128;
|
|
1282
|
+
}
|
|
1283
|
+
const { gitdir } = repo;
|
|
1284
|
+
const dir = repo.worktree ?? ctx.cwd;
|
|
1285
|
+
const cache = {};
|
|
1286
|
+
const abbrev = (oid) => oid.slice(0, 7);
|
|
1287
|
+
if (del) {
|
|
1288
|
+
let code = 0;
|
|
1289
|
+
for (const name of operands) {
|
|
1290
|
+
let oid;
|
|
1291
|
+
try {
|
|
1292
|
+
oid = await git.resolveRef({ fs, gitdir, ref: `refs/tags/${name}` });
|
|
1293
|
+
}
|
|
1294
|
+
catch {
|
|
1295
|
+
await ctx.stderr.write(`error: tag '${name}' not found.\n`);
|
|
1296
|
+
code = 1;
|
|
1297
|
+
continue;
|
|
1298
|
+
}
|
|
1299
|
+
await git.deleteTag({ fs, dir, ref: name });
|
|
1300
|
+
await ctx.stdout.write(`Deleted tag '${name}' (was ${abbrev(oid)})\n`);
|
|
1301
|
+
}
|
|
1302
|
+
return code;
|
|
1303
|
+
}
|
|
1304
|
+
const creating = !list && lines === null && operands.length > 0;
|
|
1305
|
+
if (!creating) {
|
|
1306
|
+
const patterns = operands.map(tagPattern);
|
|
1307
|
+
const names = (await git.listTags({ fs, dir }))
|
|
1308
|
+
.filter((name) => patterns.length === 0 || patterns.some((pattern) => pattern.test(name)))
|
|
1309
|
+
.sort(comparePaths);
|
|
1310
|
+
let out = '';
|
|
1311
|
+
for (const name of names) {
|
|
1312
|
+
if (lines === null) {
|
|
1313
|
+
out += `${name}\n`;
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
// The tag's own message, or for a lightweight tag its commit's (a tree or blob has none).
|
|
1317
|
+
let text = '';
|
|
1318
|
+
try {
|
|
1319
|
+
const oid = await git.resolveRef({ fs, gitdir, ref: `refs/tags/${name}` });
|
|
1320
|
+
// The raw object, split at its first blank line as git splits it: cf-git's
|
|
1321
|
+
// parsed form normalizes newlines first and misreads an empty message.
|
|
1322
|
+
const { type, object } = await git.readObject({ fs, dir, oid, cache, format: 'content' });
|
|
1323
|
+
if (type === 'tag' || type === 'commit') {
|
|
1324
|
+
const raw = dec.decode(object);
|
|
1325
|
+
const blank = raw.indexOf('\n\n');
|
|
1326
|
+
text = blank < 0 ? '' : raw.slice(blank + 2).split('\n-----BEGIN PGP SIGNATURE-----')[0];
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
catch { /* a broken ref lists bare */ }
|
|
1330
|
+
// The first <num> lines as they are, blank ones too, continuation lines indented.
|
|
1331
|
+
const shown = text.replace(/\n+$/, '').split('\n').slice(0, lines).map((line, i) => (i === 0 ? line : ` ${line}`));
|
|
1332
|
+
out += shown.length && lines > 0 ? `${name.padEnd(15)} ${shown.join('\n')}\n` : `${name}\n`;
|
|
1333
|
+
}
|
|
1334
|
+
await writeBinary(ctx.stdout, out);
|
|
1335
|
+
return 0;
|
|
1336
|
+
}
|
|
1337
|
+
const [name, target = 'HEAD', ...extra] = operands;
|
|
1338
|
+
if (extra.length > 0) {
|
|
1339
|
+
await ctx.stderr.write(`error: too many arguments\n${TAG_USAGE}`);
|
|
1340
|
+
return 129;
|
|
1341
|
+
}
|
|
1342
|
+
if (messageFile !== null) {
|
|
1343
|
+
try {
|
|
1344
|
+
messages.push(dec.decode(vfs.readFile(normalizeVfsPath(messageFile.startsWith('/') ? messageFile : `${ctx.cwd}/${messageFile}`))));
|
|
1345
|
+
}
|
|
1346
|
+
catch {
|
|
1347
|
+
await ctx.stderr.write(`fatal: could not open or read '${messageFile}': No such file or directory\n`);
|
|
1348
|
+
return 128;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
if (annotate && messages.length === 0) {
|
|
1352
|
+
// git would open an editor, and this git has none: what git says with no editor to run.
|
|
1353
|
+
await ctx.stderr.write('error: Terminal is dumb, but EDITOR unset\nPlease supply the message using either -m or -F option.\n');
|
|
1354
|
+
return 1;
|
|
1355
|
+
}
|
|
1356
|
+
const object = await resolveRevision(git, fs, gitdir, target, cache);
|
|
1357
|
+
if (!object) {
|
|
1358
|
+
await ctx.stderr.write(`fatal: Failed to resolve '${target}' as a valid ref.\n`);
|
|
1359
|
+
return 128;
|
|
1360
|
+
}
|
|
1361
|
+
let previous = null;
|
|
1362
|
+
try {
|
|
1363
|
+
previous = await git.resolveRef({ fs, gitdir, ref: `refs/tags/${name}` });
|
|
1364
|
+
}
|
|
1365
|
+
catch { /* new */ }
|
|
1366
|
+
if (previous !== null && !force) {
|
|
1367
|
+
await ctx.stderr.write(`fatal: tag '${name}' already exists\n`);
|
|
1368
|
+
return 128;
|
|
1369
|
+
}
|
|
1370
|
+
let value;
|
|
1371
|
+
if (messages.length > 0) {
|
|
1372
|
+
const ident = await gitIdent(ctx, git, fs, dir, 'COMMITTER');
|
|
1373
|
+
if ('error' in ident) {
|
|
1374
|
+
await ctx.stderr.write(ident.error);
|
|
1375
|
+
return 128;
|
|
1376
|
+
}
|
|
1377
|
+
const tagger = identLine(ident);
|
|
1378
|
+
// The object as git writes it, byte for byte: the headers, a blank line,
|
|
1379
|
+
// then the cleaned message (nothing at all when it is empty). cf-git's
|
|
1380
|
+
// annotatedTag appends a newline after the message, which an empty one
|
|
1381
|
+
// must not have, and its parser then misreads the listing.
|
|
1382
|
+
const { type } = await git.readObject({ fs, dir, oid: object, cache: {}, format: 'parsed' });
|
|
1383
|
+
const body = `object ${object}\ntype ${type}\ntag ${name}\ntagger ${tagger}\n\n${cleanupMessage(messages.join('\n\n'))}`;
|
|
1384
|
+
value = await git.writeObject({ fs, dir, type: 'tag', object: new TextEncoder().encode(body), format: 'content' });
|
|
1385
|
+
await git.writeRef({ fs, dir, ref: `refs/tags/${name}`, value, force: true });
|
|
1386
|
+
}
|
|
1387
|
+
else {
|
|
1388
|
+
await git.tag({ fs, dir, ref: name, object, force: true });
|
|
1389
|
+
value = object;
|
|
1390
|
+
}
|
|
1391
|
+
if (previous !== null && previous !== value)
|
|
1392
|
+
await ctx.stdout.write(`Updated tag '${name}' (was ${abbrev(previous)})\n`);
|
|
1393
|
+
return 0;
|
|
1394
|
+
}
|
|
1395
|
+
const LS_FILES_USAGE = 'usage: git ls-files [-c | --cached] [-o | --others] [-m | --modified] [-d | --deleted] '
|
|
1396
|
+
+ '[--exclude-standard] [-z] [--] [<path>...]\n';
|
|
1397
|
+
async function lsFiles(ctx, git, fs, vfs, args) {
|
|
1398
|
+
let cached = false;
|
|
1399
|
+
let others = false;
|
|
1400
|
+
let modified = false;
|
|
1401
|
+
let deleted = false;
|
|
1402
|
+
let excludeStandard = false;
|
|
1403
|
+
let z = false;
|
|
1404
|
+
let dashdash = false;
|
|
1405
|
+
const pathArgs = [];
|
|
1406
|
+
for (const arg of args) {
|
|
1407
|
+
if (dashdash || arg === '-' || !arg.startsWith('-')) {
|
|
1408
|
+
pathArgs.push(arg);
|
|
1409
|
+
continue;
|
|
1410
|
+
}
|
|
1411
|
+
if (arg === '--') {
|
|
1412
|
+
dashdash = true;
|
|
1413
|
+
continue;
|
|
1414
|
+
}
|
|
1415
|
+
for (const flag of arg.startsWith('--') ? [arg] : [...arg.slice(1)].map((c) => `-${c}`)) {
|
|
1416
|
+
switch (flag) {
|
|
1417
|
+
case '-c':
|
|
1418
|
+
case '--cached':
|
|
1419
|
+
cached = true;
|
|
1420
|
+
break;
|
|
1421
|
+
case '-o':
|
|
1422
|
+
case '--others':
|
|
1423
|
+
others = true;
|
|
1424
|
+
break;
|
|
1425
|
+
case '-m':
|
|
1426
|
+
case '--modified':
|
|
1427
|
+
modified = true;
|
|
1428
|
+
break;
|
|
1429
|
+
case '-d':
|
|
1430
|
+
case '--deleted':
|
|
1431
|
+
deleted = true;
|
|
1432
|
+
break;
|
|
1433
|
+
case '-z':
|
|
1434
|
+
z = true;
|
|
1435
|
+
break;
|
|
1436
|
+
case '--exclude-standard':
|
|
1437
|
+
excludeStandard = true;
|
|
1438
|
+
break;
|
|
1439
|
+
default:
|
|
1440
|
+
await ctx.stderr.write(`error: unknown option '${flag.replace(/^-+/, '')}'\n${LS_FILES_USAGE}`);
|
|
1441
|
+
return 129;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
// With no selection ls-files shows the index.
|
|
1446
|
+
if (!others && !modified && !deleted)
|
|
1447
|
+
cached = true;
|
|
1448
|
+
const repo = discoverRepo(vfs, ctx.cwd);
|
|
1449
|
+
if (!repo) {
|
|
1450
|
+
await ctx.stderr.write(NOT_A_REPOSITORY);
|
|
1451
|
+
return 128;
|
|
1452
|
+
}
|
|
1453
|
+
const root = repo.worktree;
|
|
1454
|
+
if (!root) {
|
|
1455
|
+
await ctx.stderr.write(NOT_A_WORK_TREE);
|
|
1456
|
+
return 128;
|
|
1457
|
+
}
|
|
1458
|
+
// Without pathspecs ls-files covers the cwd's subtree.
|
|
1459
|
+
const specs = pathArgs.length ? repoPaths(pathArgs, ctx.cwd, root) : [repo.prefix];
|
|
1460
|
+
const readWorktree = others || modified || deleted;
|
|
1461
|
+
const untracked = [];
|
|
1462
|
+
const tracked = [];
|
|
1463
|
+
const trees = readWorktree ? [git.STAGE(), git.WORKDIR()] : [git.STAGE()];
|
|
1464
|
+
const filemode = modified && await trustsFileMode(git, fs, root);
|
|
1465
|
+
await walkScoped(git, fs, root, {}, trees, specs, async (path, [stage, work]) => {
|
|
1466
|
+
const workType = work ? await work.type() : undefined;
|
|
1467
|
+
if (stage) {
|
|
1468
|
+
const stageType = await stage.type();
|
|
1469
|
+
if (stageType === 'tree') {
|
|
1470
|
+
// A file or link where the index has a directory is untracked; the directory's files are deleted.
|
|
1471
|
+
if (others && workType && workType !== 'tree'
|
|
1472
|
+
&& !(excludeStandard && await git.isIgnored({ fs, dir: root, filepath: path })))
|
|
1473
|
+
untracked.push(path);
|
|
1474
|
+
return true;
|
|
1475
|
+
}
|
|
1476
|
+
const missing = readWorktree && !workType;
|
|
1477
|
+
let changed = missing;
|
|
1478
|
+
if (modified && !missing && stageType === 'blob') {
|
|
1479
|
+
const stageMode = await stage.mode();
|
|
1480
|
+
changed = !work || workType !== 'blob' || await work.oid() !== await stage.oid()
|
|
1481
|
+
|| worktreeMode(await work.mode(), stageMode, filemode) !== stageMode;
|
|
1482
|
+
}
|
|
1483
|
+
tracked.push({ path, deleted: missing, modified: changed });
|
|
1484
|
+
// A file replaced by a directory leaves that directory untracked.
|
|
1485
|
+
return others && stageType === 'blob' && workType === 'tree';
|
|
1486
|
+
}
|
|
1487
|
+
if (!others || !workType)
|
|
1488
|
+
return false;
|
|
1489
|
+
const isDir = workType === 'tree';
|
|
1490
|
+
if (excludeStandard && await git.isIgnored({ fs, dir: root, filepath: isDir ? `${path}/` : path }))
|
|
1491
|
+
return false;
|
|
1492
|
+
if (!isDir) {
|
|
1493
|
+
untracked.push(path);
|
|
1494
|
+
return false;
|
|
1495
|
+
}
|
|
1496
|
+
// A nested repository is listed as the directory, never entered.
|
|
1497
|
+
if (!vfs.exists(normalizeVfsPath(`${root}/${path}/.git`)))
|
|
1498
|
+
return true;
|
|
1499
|
+
untracked.push(`${path}/`);
|
|
1500
|
+
return false;
|
|
1501
|
+
});
|
|
1502
|
+
let out = '';
|
|
1503
|
+
for (const path of untracked.sort(comparePaths))
|
|
1504
|
+
out += pathLine(relativeTo(path, repo.prefix), z);
|
|
1505
|
+
for (const entry of tracked.sort((a, b) => comparePaths(a.path, b.path))) {
|
|
1506
|
+
const line = pathLine(relativeTo(entry.path, repo.prefix), z);
|
|
1507
|
+
if (cached)
|
|
1508
|
+
out += line;
|
|
1509
|
+
if (deleted && entry.deleted)
|
|
1510
|
+
out += line;
|
|
1511
|
+
if (modified && entry.modified)
|
|
1512
|
+
out += line;
|
|
1513
|
+
}
|
|
1514
|
+
await writeBinary(ctx.stdout, out);
|
|
1515
|
+
return 0;
|
|
1516
|
+
}
|
|
1517
|
+
/**
|
|
1518
|
+
* The index entries that restoring `restored` replaces (add_index_entry_with_check):
|
|
1519
|
+
* a file at one of a restored path's leading directories, or anything below a
|
|
1520
|
+
* restored path.
|
|
1521
|
+
*/
|
|
1522
|
+
export function replacedIndexEntries(index, restored) {
|
|
1523
|
+
// Every leading directory of a restored path, once: an entry there is a file in its way.
|
|
1524
|
+
const leading = new Set();
|
|
1525
|
+
for (const path of restored) {
|
|
1526
|
+
for (let at = path.indexOf('/'); at >= 0; at = path.indexOf('/', at + 1))
|
|
1527
|
+
leading.add(path.slice(0, at));
|
|
1528
|
+
}
|
|
1529
|
+
return index.filter((path) => {
|
|
1530
|
+
if (restored.has(path))
|
|
1531
|
+
return false;
|
|
1532
|
+
if (leading.has(path))
|
|
1533
|
+
return true;
|
|
1534
|
+
for (let at = path.indexOf('/'); at >= 0; at = path.indexOf('/', at + 1)) {
|
|
1535
|
+
if (restored.has(path.slice(0, at)))
|
|
1536
|
+
return true;
|
|
1537
|
+
}
|
|
1538
|
+
return false;
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
/**
|
|
1542
|
+
* A checkout cf-git refused, as git reports one: its message on stderr, exit 1. A merge
|
|
1543
|
+
* that is not a fast-forward (`strategy`) refuses as its strategy does, with that line
|
|
1544
|
+
* after it and exit 2. Anything else propagates.
|
|
1545
|
+
*/
|
|
1546
|
+
async function refusal(ctx, error, strategy) {
|
|
1547
|
+
if (!(error instanceof Error) || !('code' in error) || error.code !== 'CheckoutConflictError')
|
|
1548
|
+
throw error;
|
|
1549
|
+
await ctx.stderr.write(`${error.message}\n${strategy ? `Merge with strategy ${strategy} failed.\n` : ''}`);
|
|
1550
|
+
return strategy ? 2 : 1;
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* `git checkout [<tree-ish>] -- <pathspec>...`: every tracked file the
|
|
1554
|
+
* pathspecs name, from the index, or from <tree-ish> into the index as well
|
|
1555
|
+
* (overlay mode: a path the tree lacks stays). A pathspec naming nothing fails
|
|
1556
|
+
* the command before any file is written, as in git. The files are written as
|
|
1557
|
+
* git's checkout writes them: see createGitFs's worktree rule.
|
|
1558
|
+
*/
|
|
1559
|
+
async function checkoutPaths(ctx, git, vfs, source, pathArgs) {
|
|
1560
|
+
const repo = discoverRepo(vfs, ctx.cwd);
|
|
1561
|
+
if (!repo) {
|
|
1562
|
+
await ctx.stderr.write(NOT_A_REPOSITORY);
|
|
1563
|
+
return 128;
|
|
1564
|
+
}
|
|
1565
|
+
const root = repo.worktree;
|
|
1566
|
+
if (!root) {
|
|
1567
|
+
await ctx.stderr.write(NOT_A_WORK_TREE);
|
|
1568
|
+
return 128;
|
|
1569
|
+
}
|
|
1570
|
+
const specs = repoPaths(pathArgs, ctx.cwd, root);
|
|
1571
|
+
// A pathspec ending in '/' names a directory: it matches what is below it, never a file or link there.
|
|
1572
|
+
const dirOnly = pathArgs.map((arg, i) => arg.endsWith('/') && specs[i] !== '');
|
|
1573
|
+
const fs = createGitFs(vfs, root);
|
|
1574
|
+
const cache = {};
|
|
1575
|
+
let tree = git.STAGE();
|
|
1576
|
+
if (source !== null) {
|
|
1577
|
+
const oid = await resolveRevision(git, fs, repo.gitdir, source, cache);
|
|
1578
|
+
if (!oid) {
|
|
1579
|
+
await ctx.stderr.write(`fatal: invalid reference: ${source}\n`);
|
|
1580
|
+
return 128;
|
|
1581
|
+
}
|
|
1582
|
+
tree = git.TREE({ ref: oid });
|
|
1583
|
+
}
|
|
1584
|
+
const files = [];
|
|
1585
|
+
const matched = new Set();
|
|
1586
|
+
await walkScoped(git, fs, root, cache, [tree], specs, async (path, [entry]) => {
|
|
1587
|
+
const type = entry ? await entry.type() : undefined;
|
|
1588
|
+
if (type === 'tree')
|
|
1589
|
+
return true;
|
|
1590
|
+
// A gitlink is never checked out.
|
|
1591
|
+
if (!entry || type !== 'blob')
|
|
1592
|
+
return false;
|
|
1593
|
+
const matching = specs.flatMap((spec, i) => spec === '' || path.startsWith(`${spec}/`) || (path === spec && !dirOnly[i]) ? [i] : []);
|
|
1594
|
+
if (matching.length === 0)
|
|
1595
|
+
return false;
|
|
1596
|
+
for (const i of matching)
|
|
1597
|
+
matched.add(i);
|
|
1598
|
+
files.push({ path, oid: await entry.oid(), mode: await entry.mode() });
|
|
1599
|
+
return false;
|
|
1600
|
+
});
|
|
1601
|
+
let unmatched = '';
|
|
1602
|
+
pathArgs.forEach((arg, i) => {
|
|
1603
|
+
if (!matched.has(i))
|
|
1604
|
+
unmatched += `error: pathspec '${arg}' did not match any file(s) known to git\n`;
|
|
1605
|
+
});
|
|
1606
|
+
if (unmatched) {
|
|
1607
|
+
await ctx.stderr.write(unmatched);
|
|
1608
|
+
return 1;
|
|
1609
|
+
}
|
|
1610
|
+
for (const { path, oid, mode } of files) {
|
|
1611
|
+
const file = `${root}/${path}`;
|
|
1612
|
+
const { blob } = await git.readBlob({ fs, dir: root, oid, cache });
|
|
1613
|
+
if (mode === 0o120000) {
|
|
1614
|
+
await fs.promises.symlink(dec.decode(blob), file);
|
|
1615
|
+
}
|
|
1616
|
+
else {
|
|
1617
|
+
await fs.promises.writeFile(file, blob);
|
|
1618
|
+
vfs.chmod(normalizeVfsPath(file), mode === 0o100755 ? 0o755 : 0o644);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
// The index takes each file's fresh stat data (and, from a tree, its blob). An entry a
|
|
1622
|
+
// restored path replaces goes first, as add_index_entry_with_check replaces it: a file at
|
|
1623
|
+
// one of its leading directories, or anything below it.
|
|
1624
|
+
const restored = new Set(files.map(({ path }) => path));
|
|
1625
|
+
const replaced = replacedIndexEntries(await git.listFiles({ fs, dir: root, cache }), restored);
|
|
1626
|
+
if (replaced.length)
|
|
1627
|
+
await git.remove({ fs, dir: root, filepath: replaced, cache });
|
|
1628
|
+
if (files.length)
|
|
1629
|
+
await git.add({ fs, dir: root, filepath: [...restored], parallel: false, force: true, cache });
|
|
1630
|
+
return 0;
|
|
1631
|
+
}
|
|
1632
|
+
/** diff-files, diff-index and diff-index --cached, as file pairs in path order. */
|
|
1633
|
+
async function changedPairs(git, fs, dir, cache, base, specs) {
|
|
1634
|
+
const pairs = [];
|
|
1635
|
+
const filemode = await trustsFileMode(git, fs, dir);
|
|
1636
|
+
// A worktree side is read against its index entry's mode.
|
|
1637
|
+
const blob = async (path, entry, index) => {
|
|
1638
|
+
if (!entry || (await entry.type()) !== 'blob')
|
|
1639
|
+
return null;
|
|
1640
|
+
const mode = await entry.mode();
|
|
1641
|
+
return index
|
|
1642
|
+
? { path, oid: await entry.oid(), mode: worktreeMode(mode, await index.mode(), filemode), worktree: true }
|
|
1643
|
+
: { path, oid: await entry.oid(), mode, worktree: false };
|
|
1644
|
+
};
|
|
1645
|
+
const record = (one, two) => {
|
|
1646
|
+
if (!one && !two)
|
|
1647
|
+
return;
|
|
1648
|
+
if (one && two && one.oid === two.oid && one.mode === two.mode)
|
|
1649
|
+
return;
|
|
1650
|
+
pairs.push({ one, two });
|
|
1651
|
+
};
|
|
1652
|
+
const trees = base.kind === 'index'
|
|
1653
|
+
? [git.STAGE(), git.WORKDIR()]
|
|
1654
|
+
: base.cached ? [git.TREE({ ref: base.ref }), git.STAGE()] : [git.TREE({ ref: base.ref }), git.STAGE(), git.WORKDIR()];
|
|
1655
|
+
await walkScoped(git, fs, dir, cache, trees, specs, async (path, entries) => {
|
|
1656
|
+
if (base.kind === 'index') {
|
|
1657
|
+
const [stage, work] = entries;
|
|
1658
|
+
const stageType = stage ? await stage.type() : undefined;
|
|
1659
|
+
if (stageType !== 'blob')
|
|
1660
|
+
return stageType === 'tree';
|
|
1661
|
+
// A missing file, or a directory where the file was, is a deletion.
|
|
1662
|
+
record(await blob(path, stage), await blob(path, work, stage));
|
|
1663
|
+
return false;
|
|
1664
|
+
}
|
|
1665
|
+
const [head, stage, work] = entries;
|
|
1666
|
+
const headType = head ? await head.type() : undefined;
|
|
1667
|
+
const stageType = stage ? await stage.type() : undefined;
|
|
1668
|
+
// diff-index: a path the index lacks is deleted whatever the worktree holds.
|
|
1669
|
+
const two = stageType !== 'blob' ? null : base.cached ? await blob(path, stage) : await blob(path, work, stage);
|
|
1670
|
+
record(await blob(path, head), two);
|
|
1671
|
+
return headType === 'tree' || stageType === 'tree';
|
|
1672
|
+
});
|
|
1673
|
+
const pathOf = (pair) => (pair.one ?? pair.two).path;
|
|
1674
|
+
return pairs.sort((a, b) => comparePaths(pathOf(a), pathOf(b)));
|
|
1675
|
+
}
|
|
1676
|
+
/** Print pairs one at a time, each loaded only while it is rendered. */
|
|
1677
|
+
async function writeDiff(ctx, pairs, output) {
|
|
1678
|
+
const stats = [];
|
|
1679
|
+
let out = '';
|
|
1680
|
+
for (const load of pairs) {
|
|
1681
|
+
const pair = await load();
|
|
1682
|
+
if (output.format === 'stat')
|
|
1683
|
+
stats.push(statFile(pair));
|
|
1684
|
+
else if (output.format === 'name-only')
|
|
1685
|
+
out += formatNameOnly(pair, output.z);
|
|
1686
|
+
else if (output.format === 'name-status')
|
|
1687
|
+
out += formatNameStatus(pair, output.z);
|
|
1688
|
+
else
|
|
1689
|
+
out += formatPatch(pair, output.context);
|
|
1690
|
+
if (out.length >= 1 << 16) {
|
|
1691
|
+
await writeBinary(ctx.stdout, out);
|
|
1692
|
+
out = '';
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
if (output.format === 'stat')
|
|
1696
|
+
out += formatStat(stats, output.columns);
|
|
1697
|
+
await writeBinary(ctx.stdout, out);
|
|
1698
|
+
}
|
|
1699
|
+
/** `git diff --no-index`: two paths, either of them /dev/null; exits 1 when they differ. */
|
|
1700
|
+
async function diffNoIndex(ctx, git, vfs, paths, output) {
|
|
1701
|
+
if (paths.length !== 2) {
|
|
1702
|
+
await ctx.stderr.write('usage: git diff --no-index [<options>] <path> <path>\n');
|
|
1703
|
+
return 129;
|
|
1704
|
+
}
|
|
1705
|
+
const key = (path) => normalizeVfsPath(path.startsWith('/') ? path : `${ctx.cwd}/${path}`);
|
|
1706
|
+
const isDir = paths.map((path) => path !== '/dev/null' && vfs.isDirectory(key(path)));
|
|
1707
|
+
if (isDir[0] && isDir[1]) {
|
|
1708
|
+
await ctx.stderr.write('error: --no-index between two directories is not supported\n');
|
|
1709
|
+
return 129;
|
|
1710
|
+
}
|
|
1711
|
+
// fixup_paths: a directory against a file means that file's namesake inside it.
|
|
1712
|
+
if (isDir[0] !== isDir[1]) {
|
|
1713
|
+
const dirSide = isDir[0] ? 0 : 1;
|
|
1714
|
+
const file = paths[1 - dirSide];
|
|
1715
|
+
paths[dirSide] = `${paths[dirSide].replace(/\/+$/, '')}/${file.slice(file.lastIndexOf('/') + 1)}`;
|
|
1716
|
+
}
|
|
1717
|
+
const specs = [];
|
|
1718
|
+
for (const path of paths) {
|
|
1719
|
+
if (path === '/dev/null') {
|
|
1720
|
+
specs.push(absentSpec(path));
|
|
1721
|
+
continue;
|
|
1722
|
+
}
|
|
1723
|
+
let st;
|
|
1724
|
+
try {
|
|
1725
|
+
st = vfs.lstat(key(path));
|
|
1726
|
+
}
|
|
1727
|
+
catch {
|
|
1728
|
+
await ctx.stderr.write(`error: Could not access '${path}'\n`);
|
|
1729
|
+
return 1;
|
|
1730
|
+
}
|
|
1731
|
+
const link = st.type === 'symlink';
|
|
1732
|
+
const data = link ? enc.encode(vfs.readlink(key(path))) : vfs.readFile(key(path));
|
|
1733
|
+
const { oid } = await git.hashBlob({ object: data });
|
|
1734
|
+
// canon_mode: the owner's execute bit alone decides 100755.
|
|
1735
|
+
const mode = link ? 0o120000 : st.mode & 0o100 ? 0o100755 : 0o100644;
|
|
1736
|
+
specs.push({ path, valid: true, oid, mode, data });
|
|
1737
|
+
}
|
|
1738
|
+
const [one, two] = specs;
|
|
1739
|
+
if (!one.valid && !two.valid)
|
|
1740
|
+
return 0;
|
|
1741
|
+
if (one.valid && two.valid && one.oid === two.oid && one.mode === two.mode)
|
|
1742
|
+
return 0;
|
|
1743
|
+
await writeDiff(ctx, [async () => ({ one, two })], output);
|
|
1744
|
+
return 1;
|
|
1745
|
+
}
|
|
1746
|
+
const DIFF_USAGE = 'usage: git diff [--cached] [<commit>] [--] [<path>...]\n'
|
|
1747
|
+
+ ' or: git diff --no-index [--] <path> <path>\n'
|
|
1748
|
+
+ 'options: --stat | --name-only | --name-status, -z, -U<n>, -M[<n>] | --no-renames\n';
|
|
1749
|
+
async function diffCommand(ctx, git, fs, vfs, args) {
|
|
1750
|
+
let cached = false;
|
|
1751
|
+
let noIndex = false;
|
|
1752
|
+
let dashdash = false;
|
|
1753
|
+
// Renames are on by default, as with git's diff.renames; null turns them off.
|
|
1754
|
+
let minimumScore = DEFAULT_RENAME_SCORE;
|
|
1755
|
+
const output = {
|
|
1756
|
+
format: 'patch',
|
|
1757
|
+
z: false,
|
|
1758
|
+
context: DEFAULT_CONTEXT,
|
|
1759
|
+
columns: parseInt(ctx.env.COLUMNS ?? '', 10) > 0 ? parseInt(ctx.env.COLUMNS, 10) : 80,
|
|
1760
|
+
};
|
|
1761
|
+
const positionals = [];
|
|
1762
|
+
const pathArgs = [];
|
|
1763
|
+
for (const arg of args) {
|
|
1764
|
+
if (dashdash) {
|
|
1765
|
+
pathArgs.push(arg);
|
|
1766
|
+
continue;
|
|
1767
|
+
}
|
|
1768
|
+
if (arg === '--') {
|
|
1769
|
+
dashdash = true;
|
|
1770
|
+
continue;
|
|
1771
|
+
}
|
|
1772
|
+
if (arg === '-' || !arg.startsWith('-')) {
|
|
1773
|
+
positionals.push(arg);
|
|
1774
|
+
continue;
|
|
1775
|
+
}
|
|
1776
|
+
const context = /^(?:-U|--unified=)(\d+)$/.exec(arg);
|
|
1777
|
+
if (context) {
|
|
1778
|
+
output.context = Number(context[1]);
|
|
1779
|
+
continue;
|
|
1780
|
+
}
|
|
1781
|
+
const findRenames = /^(?:-M|--find-renames(?:=|$))(.*)$/.exec(arg);
|
|
1782
|
+
if (findRenames) {
|
|
1783
|
+
const score = parseRenameScore(findRenames[1]);
|
|
1784
|
+
if (score === null) {
|
|
1785
|
+
await ctx.stderr.write(`error: invalid argument to find-renames\n${DIFF_USAGE}`);
|
|
1786
|
+
return 129;
|
|
1787
|
+
}
|
|
1788
|
+
minimumScore = score || DEFAULT_RENAME_SCORE;
|
|
1789
|
+
continue;
|
|
1790
|
+
}
|
|
1791
|
+
switch (arg) {
|
|
1792
|
+
case '--cached':
|
|
1793
|
+
case '--staged':
|
|
1794
|
+
cached = true;
|
|
1795
|
+
continue;
|
|
1796
|
+
case '--no-index':
|
|
1797
|
+
noIndex = true;
|
|
1798
|
+
continue;
|
|
1799
|
+
case '-z':
|
|
1800
|
+
output.z = true;
|
|
1801
|
+
continue;
|
|
1802
|
+
case '--no-renames':
|
|
1803
|
+
minimumScore = null;
|
|
1804
|
+
continue;
|
|
1805
|
+
// A patch is the default, and nothing here colors or runs external tools.
|
|
1806
|
+
case '-p':
|
|
1807
|
+
case '-u':
|
|
1808
|
+
case '--patch':
|
|
1809
|
+
case '--no-ext-diff':
|
|
1810
|
+
case '--no-color': continue;
|
|
1811
|
+
case '--stat':
|
|
1812
|
+
case '--name-only':
|
|
1813
|
+
case '--name-status': {
|
|
1814
|
+
const format = arg.slice(2);
|
|
1815
|
+
if (output.format !== 'patch' && output.format !== format) {
|
|
1816
|
+
await ctx.stderr.write(`fatal: options '--${output.format}' and '${arg}' cannot be used together\n`);
|
|
1817
|
+
return 128;
|
|
1818
|
+
}
|
|
1819
|
+
output.format = format;
|
|
1820
|
+
continue;
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
await ctx.stderr.write(`error: unknown option '${arg.replace(/^-+/, '')}'\n${DIFF_USAGE}`);
|
|
1824
|
+
return 129;
|
|
1825
|
+
}
|
|
1826
|
+
if (noIndex) {
|
|
1827
|
+
if (cached) {
|
|
1828
|
+
await ctx.stderr.write("fatal: options '--cached' and '--no-index' cannot be used together\n");
|
|
1829
|
+
return 128;
|
|
1830
|
+
}
|
|
1831
|
+
return diffNoIndex(ctx, git, vfs, [...positionals, ...pathArgs], output);
|
|
1832
|
+
}
|
|
1833
|
+
const repo = discoverRepo(vfs, ctx.cwd);
|
|
1834
|
+
if (!repo) {
|
|
1835
|
+
await ctx.stderr.write(NOT_A_REPOSITORY);
|
|
1836
|
+
return 128;
|
|
1837
|
+
}
|
|
1838
|
+
const root = repo.worktree;
|
|
1839
|
+
if (!root) {
|
|
1840
|
+
await ctx.stderr.write(NOT_A_WORK_TREE);
|
|
1841
|
+
return 128;
|
|
1842
|
+
}
|
|
1843
|
+
const cache = {};
|
|
1844
|
+
const revs = [];
|
|
1845
|
+
for (const arg of positionals) {
|
|
1846
|
+
// Before `--` a word is a revision until one is not; then it and the rest must be paths.
|
|
1847
|
+
const oid = pathArgs.length && !dashdash ? null : await resolveRevision(git, fs, repo.gitdir, arg, cache);
|
|
1848
|
+
if (oid !== null) {
|
|
1849
|
+
revs.push(oid);
|
|
1850
|
+
continue;
|
|
1851
|
+
}
|
|
1852
|
+
if (dashdash) {
|
|
1853
|
+
await ctx.stderr.write(`fatal: bad revision '${arg}'\n`);
|
|
1854
|
+
return 128;
|
|
1855
|
+
}
|
|
1856
|
+
if (!vfs.exists(normalizeVfsPath(arg.startsWith('/') ? arg : `${ctx.cwd}/${arg}`))) {
|
|
1857
|
+
await ctx.stderr.write(ambiguousArgument(arg));
|
|
1858
|
+
return 128;
|
|
1859
|
+
}
|
|
1860
|
+
pathArgs.push(arg);
|
|
1861
|
+
}
|
|
1862
|
+
if (revs.length > 1) {
|
|
1863
|
+
await ctx.stderr.write('fatal: diff between two commits is not supported; compare one commit with the worktree or the index\n');
|
|
1864
|
+
return 128;
|
|
1865
|
+
}
|
|
1866
|
+
const base = cached
|
|
1867
|
+
? { kind: 'tree', ref: revs[0] ?? 'HEAD', cached: true }
|
|
1868
|
+
: revs.length ? { kind: 'tree', ref: revs[0], cached: false } : { kind: 'index' };
|
|
1869
|
+
const pending = await changedPairs(git, fs, root, cache, base, repoPaths(pathArgs, ctx.cwd, root));
|
|
1870
|
+
const read = async (side) => {
|
|
1871
|
+
if (!side.worktree)
|
|
1872
|
+
return (await git.readBlob({ fs, dir: root, oid: side.oid, cache })).blob;
|
|
1873
|
+
const key = normalizeVfsPath(`${root}/${side.path}`);
|
|
1874
|
+
return side.mode === 0o120000 ? enc.encode(vfs.readlink(key)) : vfs.readFile(key);
|
|
1875
|
+
};
|
|
1876
|
+
const { queue, neededRenameLimit } = minimumScore === null
|
|
1877
|
+
? { queue: pending, neededRenameLimit: 0 }
|
|
1878
|
+
: await detectRenames(pending, read, { minimumScore });
|
|
1879
|
+
const withData = output.format === 'patch' || output.format === 'stat';
|
|
1880
|
+
const spec = async (side) => ({
|
|
1881
|
+
path: side.path,
|
|
1882
|
+
valid: true,
|
|
1883
|
+
oid: side.oid,
|
|
1884
|
+
mode: side.mode,
|
|
1885
|
+
data: withData ? await read(side) : new Uint8Array(0),
|
|
1886
|
+
});
|
|
1887
|
+
await writeDiff(ctx, queue.map(({ one, two, renameScore }) => async () => ({
|
|
1888
|
+
one: one ? await spec(one) : absentSpec(two.path),
|
|
1889
|
+
two: two ? await spec(two) : absentSpec(one.path),
|
|
1890
|
+
renameScore,
|
|
1891
|
+
})), output);
|
|
1892
|
+
if (neededRenameLimit) {
|
|
1893
|
+
await ctx.stderr.write('warning: exhaustive rename detection was skipped due to too many files.\n'
|
|
1894
|
+
+ `warning: you may want to set your diff.renameLimit variable to at least ${neededRenameLimit} and retry the command.\n`);
|
|
1895
|
+
}
|
|
1896
|
+
return 0;
|
|
246
1897
|
}
|
|
247
1898
|
// ── Git subcommand implementations ──────────────────────────────────────
|
|
248
1899
|
/**
|
|
@@ -254,10 +1905,18 @@ function getAuthor(ctx) {
|
|
|
254
1905
|
export async function runGitCommand(ctx, vfs, doCtx, doEnv) {
|
|
255
1906
|
const credentialedVfs = vfs.as(requireVfsCred(ctx.cred, 'git'));
|
|
256
1907
|
const fs = createGitFs(credentialedVfs);
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
1908
|
+
let globals;
|
|
1909
|
+
try {
|
|
1910
|
+
globals = parseGitGlobals(ctx.args, getDir(ctx));
|
|
1911
|
+
}
|
|
1912
|
+
catch (e) {
|
|
1913
|
+
ctx.stderr.write(`git: ${e?.message}\n`);
|
|
1914
|
+
return 129;
|
|
1915
|
+
}
|
|
1916
|
+
const { sub, subArgs, dir } = globals;
|
|
1917
|
+
// Every subcommand below reads `dir` and the clone's `getDir(ctx)`; `-C`
|
|
1918
|
+
// moves both, exactly as `git -C <path>` runs the command from <path>.
|
|
1919
|
+
ctx = { ...ctx, cwd: dir };
|
|
261
1920
|
if (sub === '--version' || sub === '-v') {
|
|
262
1921
|
ctx.stdout.write('git version 2.44.0 (isomorphic-git/cf-git)\n');
|
|
263
1922
|
return 0;
|
|
@@ -266,8 +1925,8 @@ export async function runGitCommand(ctx, vfs, doCtx, doEnv) {
|
|
|
266
1925
|
ctx.stdout.write('usage: git <command> [<args>]\n\n');
|
|
267
1926
|
ctx.stdout.write('Commands:\n');
|
|
268
1927
|
ctx.stdout.write(' init, clone, status, add, commit, log, branch,\n');
|
|
269
|
-
ctx.stdout.write(' checkout, diff,
|
|
270
|
-
ctx.stdout.write(' reset, tag, config, --version\n');
|
|
1928
|
+
ctx.stdout.write(' checkout, diff, ls-files, rev-parse, remote,\n');
|
|
1929
|
+
ctx.stdout.write(' fetch, pull, push, merge, reset, tag, config, --version\n');
|
|
271
1930
|
return 0;
|
|
272
1931
|
}
|
|
273
1932
|
// Lazy-load isomorphic-git only when actually needed.
|
|
@@ -295,11 +1954,14 @@ export async function runGitCommand(ctx, vfs, doCtx, doEnv) {
|
|
|
295
1954
|
credentialedVfs.mkdir(stripped, { recursive: true });
|
|
296
1955
|
}
|
|
297
1956
|
await git.init({ fs, dir: initDir });
|
|
298
|
-
|
|
1957
|
+
if (!subArgs.includes('-q') && !subArgs.includes('--quiet')) {
|
|
1958
|
+
ctx.stdout.write(`Initialized empty Git repository in ${initDir}/.git/\n`);
|
|
1959
|
+
}
|
|
299
1960
|
return 0;
|
|
300
1961
|
}
|
|
301
1962
|
case 'clone': {
|
|
302
|
-
const { url, dest: destArg, depth, isBg, branch } = parseCloneArgs(subArgs);
|
|
1963
|
+
const { url, dest: destArg, depth, isBg, branch, quiet } = parseCloneArgs(subArgs);
|
|
1964
|
+
const progress = quiet ? { write() { } } : ctx.stdout;
|
|
303
1965
|
if (!url) {
|
|
304
1966
|
ctx.stderr.write(CLONE_USAGE + '\n');
|
|
305
1967
|
return 1;
|
|
@@ -322,7 +1984,7 @@ export async function runGitCommand(ctx, vfs, doCtx, doEnv) {
|
|
|
322
1984
|
ctx.stderr.write('[git] clone requires DO ctx + env (internal configuration error)\n');
|
|
323
1985
|
return 1;
|
|
324
1986
|
}
|
|
325
|
-
|
|
1987
|
+
progress.write(`Cloning into '${dest}'...${depth ? ' (shallow, depth=' + depth + ')' : ''}\n`);
|
|
326
1988
|
// A clone's closed-world filesystem view is correct only while no
|
|
327
1989
|
// other session surface can mutate its destination subtree. Acquire
|
|
328
1990
|
// the lease before the facet performs its lstat/readdir emptiness
|
|
@@ -342,6 +2004,7 @@ export async function runGitCommand(ctx, vfs, doCtx, doEnv) {
|
|
|
342
2004
|
url,
|
|
343
2005
|
ref: branch,
|
|
344
2006
|
depth,
|
|
2007
|
+
quiet,
|
|
345
2008
|
exclusiveDestination: true,
|
|
346
2009
|
exclusiveMutationRoot: mutationLease.root,
|
|
347
2010
|
mutationOwner: mutationLease.owner,
|
|
@@ -357,7 +2020,7 @@ export async function runGitCommand(ctx, vfs, doCtx, doEnv) {
|
|
|
357
2020
|
},
|
|
358
2021
|
});
|
|
359
2022
|
if (result.success) {
|
|
360
|
-
|
|
2023
|
+
progress.write(`\n[git] clone complete (${result.filesWritten} files, ` +
|
|
361
2024
|
`${(result.bytesWritten / 1024).toFixed(1)}KB in ${(result.elapsed / 1000).toFixed(1)}s)\n`);
|
|
362
2025
|
}
|
|
363
2026
|
else {
|
|
@@ -372,7 +2035,7 @@ export async function runGitCommand(ctx, vfs, doCtx, doEnv) {
|
|
|
372
2035
|
if (isBg) {
|
|
373
2036
|
const task = doClone();
|
|
374
2037
|
doCtx.waitUntil(task);
|
|
375
|
-
|
|
2038
|
+
progress.write('[git] clone running in background...\n');
|
|
376
2039
|
return 0;
|
|
377
2040
|
}
|
|
378
2041
|
else {
|
|
@@ -405,41 +2068,47 @@ export async function runGitCommand(ctx, vfs, doCtx, doEnv) {
|
|
|
405
2068
|
ctx.stdout.write('nothing to commit, working tree clean\n');
|
|
406
2069
|
return 0;
|
|
407
2070
|
}
|
|
408
|
-
case 'add':
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
await git.add({ fs, dir, filepath });
|
|
419
|
-
}
|
|
2071
|
+
case 'add':
|
|
2072
|
+
return await addCommand(ctx, git, fs, credentialedVfs, subArgs);
|
|
2073
|
+
case 'commit': {
|
|
2074
|
+
const { messages, quiet, all, cleanup, allowEmptyMessage } = parseCommitArgs(subArgs);
|
|
2075
|
+
let configured;
|
|
2076
|
+
if (cleanup === null) {
|
|
2077
|
+
try {
|
|
2078
|
+
const value = await git.getConfig({ fs, dir, path: 'commit.cleanup' });
|
|
2079
|
+
if (typeof value === 'string')
|
|
2080
|
+
configured = value;
|
|
420
2081
|
}
|
|
2082
|
+
catch { /* unset */ }
|
|
421
2083
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
}
|
|
2084
|
+
const requested = cleanup ?? configured;
|
|
2085
|
+
const mode = commitCleanupMode(requested);
|
|
2086
|
+
if (!mode) {
|
|
2087
|
+
await ctx.stderr.write(`fatal: Invalid cleanup mode ${requested}\n`);
|
|
2088
|
+
return 128;
|
|
426
2089
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const message = msgIdx >= 0 ? subArgs[msgIdx + 1] : 'commit';
|
|
432
|
-
if (!message) {
|
|
433
|
-
ctx.stderr.write('error: empty commit message\n');
|
|
2090
|
+
const raw = messages.length ? joinMessageOptions(messages) : 'commit\n';
|
|
2091
|
+
const message = mode === 'verbatim' ? raw : cleanupMessage(raw, mode === 'strip');
|
|
2092
|
+
if (!message && !allowEmptyMessage) {
|
|
2093
|
+
await ctx.stderr.write('Aborting commit due to empty commit message.\n');
|
|
434
2094
|
return 1;
|
|
435
2095
|
}
|
|
436
|
-
|
|
437
|
-
fs, dir
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
2096
|
+
if (all)
|
|
2097
|
+
await stageTracked(git, fs, dir);
|
|
2098
|
+
const idents = await commitIdents(ctx, git, fs, dir);
|
|
2099
|
+
if ('error' in idents) {
|
|
2100
|
+
await ctx.stderr.write(idents.error);
|
|
2101
|
+
return 128;
|
|
2102
|
+
}
|
|
2103
|
+
const sha = await writeCommit(git, fs, dir, message, idents);
|
|
2104
|
+
if (!quiet)
|
|
2105
|
+
ctx.stdout.write(`[${sha.slice(0, 7)}] ${message.split('\n')[0]}\n`);
|
|
441
2106
|
return 0;
|
|
442
2107
|
}
|
|
2108
|
+
case 'rev-parse':
|
|
2109
|
+
return await revParse(ctx, git, fs, credentialedVfs, subArgs);
|
|
2110
|
+
case 'ls-files':
|
|
2111
|
+
return await lsFiles(ctx, git, fs, credentialedVfs, subArgs);
|
|
443
2112
|
case 'log': {
|
|
444
2113
|
const maxCount = parseInt(getFlag(subArgs, '-n') || getFlag(subArgs, '--max-count') || '10');
|
|
445
2114
|
const oneline = subArgs.includes('--oneline');
|
|
@@ -458,6 +2127,18 @@ export async function runGitCommand(ctx, vfs, doCtx, doEnv) {
|
|
|
458
2127
|
return 0;
|
|
459
2128
|
}
|
|
460
2129
|
case 'branch': {
|
|
2130
|
+
if (subArgs[0] === '--show-current') {
|
|
2131
|
+
// Empty output on a detached HEAD, like git.
|
|
2132
|
+
const current = await git.currentBranch({ fs, dir });
|
|
2133
|
+
if (current)
|
|
2134
|
+
ctx.stdout.write(`${current}\n`);
|
|
2135
|
+
return 0;
|
|
2136
|
+
}
|
|
2137
|
+
const unknown = subArgs.find((a) => a.startsWith('-') && !['-a', '--list', '-d', '-D'].includes(a));
|
|
2138
|
+
if (unknown) {
|
|
2139
|
+
ctx.stderr.write(`error: unknown option '${unknown}'\nusage: git branch [-a | --list | --show-current | -d <name> | -D <name> | <name>]\n`);
|
|
2140
|
+
return 129;
|
|
2141
|
+
}
|
|
461
2142
|
if (subArgs.length === 0 || subArgs[0] === '-a' || subArgs[0] === '--list') {
|
|
462
2143
|
const branches = await git.listBranches({ fs, dir });
|
|
463
2144
|
const current = await git.currentBranch({ fs, dir });
|
|
@@ -488,47 +2169,38 @@ export async function runGitCommand(ctx, vfs, doCtx, doEnv) {
|
|
|
488
2169
|
return 0;
|
|
489
2170
|
}
|
|
490
2171
|
case 'checkout': {
|
|
491
|
-
|
|
2172
|
+
// `--` with paths after it restores those paths; a bare `--` only ends the options.
|
|
2173
|
+
const dashdash = subArgs.indexOf('--');
|
|
2174
|
+
const options = dashdash >= 0 ? subArgs.slice(0, dashdash) : subArgs;
|
|
2175
|
+
if (dashdash >= 0 && dashdash < subArgs.length - 1) {
|
|
2176
|
+
const source = options.find(a => !a.startsWith('-'));
|
|
2177
|
+
return await checkoutPaths(ctx, git, credentialedVfs, source ?? null, subArgs.slice(dashdash + 1));
|
|
2178
|
+
}
|
|
2179
|
+
const quiet = options.includes('-q') || options.includes('--quiet');
|
|
2180
|
+
const ref = options.find(a => !a.startsWith('-'));
|
|
2181
|
+
// `git checkout` and `git checkout HEAD` switch to where HEAD already is: nothing changes.
|
|
2182
|
+
if ((!ref || ref === 'HEAD') && !options.includes('-b'))
|
|
2183
|
+
return 0;
|
|
492
2184
|
if (!ref) {
|
|
493
|
-
ctx.stderr.write(
|
|
494
|
-
return
|
|
2185
|
+
ctx.stderr.write("error: switch `b' requires a value\n");
|
|
2186
|
+
return 129;
|
|
495
2187
|
}
|
|
496
|
-
|
|
2188
|
+
const worktreeFs = createGitFs(credentialedVfs, dir);
|
|
2189
|
+
const create = options.includes('-b');
|
|
2190
|
+
if (create)
|
|
497
2191
|
await git.branch({ fs, dir, ref });
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
501
|
-
else {
|
|
502
|
-
await git.checkout({ fs, dir, ref });
|
|
503
|
-
ctx.stdout.write(`Switched to branch '${ref}'\n`);
|
|
2192
|
+
try {
|
|
2193
|
+
await git.checkout({ fs: worktreeFs, dir, ref });
|
|
504
2194
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
case 'diff': {
|
|
508
|
-
// Simple diff: show unstaged changes
|
|
509
|
-
const matrix = await git.statusMatrix({ fs, dir });
|
|
510
|
-
for (const [filepath, head, workdir, stage] of matrix) {
|
|
511
|
-
if (workdir !== head || workdir !== stage) {
|
|
512
|
-
ctx.stdout.write(`\x1b[1mdiff --git a/${filepath} b/${filepath}\x1b[0m\n`);
|
|
513
|
-
try {
|
|
514
|
-
const raw = await fs.promises.readFile(dir + '/' + filepath);
|
|
515
|
-
const content = typeof raw === 'string' ? raw : dec.decode(raw);
|
|
516
|
-
const lines = content.split('\n');
|
|
517
|
-
for (let i = 0; i < Math.min(lines.length, 50); i++) {
|
|
518
|
-
if (head === 0)
|
|
519
|
-
ctx.stdout.write(`\x1b[32m+${lines[i]}\x1b[0m\n`);
|
|
520
|
-
else
|
|
521
|
-
ctx.stdout.write(` ${lines[i]}\n`);
|
|
522
|
-
}
|
|
523
|
-
if (lines.length > 50)
|
|
524
|
-
ctx.stdout.write(`... (${lines.length - 50} more lines)\n`);
|
|
525
|
-
}
|
|
526
|
-
catch { }
|
|
527
|
-
ctx.stdout.write('\n');
|
|
528
|
-
}
|
|
2195
|
+
catch (e) {
|
|
2196
|
+
return await refusal(ctx, e);
|
|
529
2197
|
}
|
|
2198
|
+
if (!quiet)
|
|
2199
|
+
ctx.stdout.write(create ? `Switched to a new branch '${ref}'\n` : `Switched to branch '${ref}'\n`);
|
|
530
2200
|
return 0;
|
|
531
2201
|
}
|
|
2202
|
+
case 'diff':
|
|
2203
|
+
return await diffCommand(ctx, git, fs, credentialedVfs, subArgs);
|
|
532
2204
|
case 'remote': {
|
|
533
2205
|
if (subArgs[0] === 'add' && subArgs[1] && subArgs[2]) {
|
|
534
2206
|
await git.addRemote({ fs, dir, remote: subArgs[1], url: subArgs[2] });
|
|
@@ -547,24 +2219,28 @@ export async function runGitCommand(ctx, vfs, doCtx, doEnv) {
|
|
|
547
2219
|
return 0;
|
|
548
2220
|
}
|
|
549
2221
|
case 'fetch': {
|
|
550
|
-
const
|
|
2222
|
+
const { quiet, rest } = takeQuiet(subArgs);
|
|
2223
|
+
const remote = rest[0] || 'origin';
|
|
551
2224
|
if (!doCtx || !doEnv) {
|
|
552
2225
|
ctx.stderr.write('[git] fetch requires DO ctx + env (internal configuration error)\n');
|
|
553
2226
|
return 1;
|
|
554
2227
|
}
|
|
555
|
-
|
|
2228
|
+
if (!quiet)
|
|
2229
|
+
ctx.stdout.write(`Fetching from ${remote}...\n`);
|
|
556
2230
|
const result = await execGitNetwork(doCtx, doEnv, {
|
|
557
2231
|
op: 'fetch',
|
|
558
2232
|
pid: ctx.pid,
|
|
559
2233
|
dir,
|
|
560
2234
|
remote,
|
|
2235
|
+
quiet,
|
|
561
2236
|
auth: {
|
|
562
2237
|
username: ctx.env.GIT_USERNAME || '',
|
|
563
2238
|
password: ctx.env.GIT_PASSWORD || ctx.env.GIT_TOKEN || '',
|
|
564
2239
|
},
|
|
565
2240
|
});
|
|
566
2241
|
if (result.success) {
|
|
567
|
-
|
|
2242
|
+
if (!quiet)
|
|
2243
|
+
ctx.stdout.write(`\n[git] fetch complete (${result.filesWritten} files in ${(result.elapsed / 1000).toFixed(1)}s)\n`);
|
|
568
2244
|
return 0;
|
|
569
2245
|
}
|
|
570
2246
|
else {
|
|
@@ -573,27 +2249,36 @@ export async function runGitCommand(ctx, vfs, doCtx, doEnv) {
|
|
|
573
2249
|
}
|
|
574
2250
|
}
|
|
575
2251
|
case 'pull': {
|
|
576
|
-
const
|
|
577
|
-
const
|
|
2252
|
+
const { quiet, rest } = takeQuiet(subArgs);
|
|
2253
|
+
const remote = rest[0] || 'origin';
|
|
2254
|
+
const branch = rest[1] || await git.currentBranch({ fs, dir }) || 'main';
|
|
578
2255
|
if (!doCtx || !doEnv) {
|
|
579
2256
|
ctx.stderr.write('[git] pull requires DO ctx + env (internal configuration error)\n');
|
|
580
2257
|
return 1;
|
|
581
2258
|
}
|
|
582
|
-
ctx
|
|
2259
|
+
const pullIdents = await commitIdents(ctx, git, fs, dir);
|
|
2260
|
+
if ('error' in pullIdents) {
|
|
2261
|
+
await ctx.stderr.write(pullIdents.error);
|
|
2262
|
+
return 128;
|
|
2263
|
+
}
|
|
2264
|
+
if (!quiet)
|
|
2265
|
+
ctx.stdout.write(`Pulling from ${remote}/${branch}...\n`);
|
|
583
2266
|
const result = await execGitNetwork(doCtx, doEnv, {
|
|
584
2267
|
op: 'pull',
|
|
585
2268
|
pid: ctx.pid,
|
|
586
2269
|
dir,
|
|
587
2270
|
remote,
|
|
588
2271
|
ref: branch,
|
|
589
|
-
|
|
2272
|
+
quiet,
|
|
2273
|
+
...pullIdents,
|
|
590
2274
|
auth: {
|
|
591
2275
|
username: ctx.env.GIT_USERNAME || '',
|
|
592
2276
|
password: ctx.env.GIT_PASSWORD || ctx.env.GIT_TOKEN || '',
|
|
593
2277
|
},
|
|
594
2278
|
});
|
|
595
2279
|
if (result.success) {
|
|
596
|
-
|
|
2280
|
+
if (!quiet)
|
|
2281
|
+
ctx.stdout.write(`\n[git] pull complete (${result.filesWritten} files in ${(result.elapsed / 1000).toFixed(1)}s)\n`);
|
|
597
2282
|
return 0;
|
|
598
2283
|
}
|
|
599
2284
|
else {
|
|
@@ -602,26 +2287,30 @@ export async function runGitCommand(ctx, vfs, doCtx, doEnv) {
|
|
|
602
2287
|
}
|
|
603
2288
|
}
|
|
604
2289
|
case 'push': {
|
|
605
|
-
const
|
|
606
|
-
const
|
|
2290
|
+
const { quiet, rest } = takeQuiet(subArgs);
|
|
2291
|
+
const remote = rest[0] || 'origin';
|
|
2292
|
+
const branch = rest[1] || await git.currentBranch({ fs, dir }) || 'main';
|
|
607
2293
|
if (!doCtx || !doEnv) {
|
|
608
2294
|
ctx.stderr.write('[git] push requires DO ctx + env (internal configuration error)\n');
|
|
609
2295
|
return 1;
|
|
610
2296
|
}
|
|
611
|
-
|
|
2297
|
+
if (!quiet)
|
|
2298
|
+
ctx.stdout.write(`Pushing to ${remote}/${branch}...\n`);
|
|
612
2299
|
const result = await execGitNetwork(doCtx, doEnv, {
|
|
613
2300
|
op: 'push',
|
|
614
2301
|
pid: ctx.pid,
|
|
615
2302
|
dir,
|
|
616
2303
|
remote,
|
|
617
2304
|
ref: branch,
|
|
2305
|
+
quiet,
|
|
618
2306
|
auth: {
|
|
619
2307
|
username: ctx.env.GIT_USERNAME || '',
|
|
620
2308
|
password: ctx.env.GIT_PASSWORD || ctx.env.GIT_TOKEN || '',
|
|
621
2309
|
},
|
|
622
2310
|
});
|
|
623
2311
|
if (result.success) {
|
|
624
|
-
|
|
2312
|
+
if (!quiet)
|
|
2313
|
+
ctx.stdout.write(`\n[git] push complete (${(result.elapsed / 1000).toFixed(1)}s)\n`);
|
|
625
2314
|
return 0;
|
|
626
2315
|
}
|
|
627
2316
|
else {
|
|
@@ -630,16 +2319,35 @@ export async function runGitCommand(ctx, vfs, doCtx, doEnv) {
|
|
|
630
2319
|
}
|
|
631
2320
|
}
|
|
632
2321
|
case 'merge': {
|
|
633
|
-
const theirs = subArgs
|
|
2322
|
+
const theirs = subArgs.find(a => !a.startsWith('-'));
|
|
634
2323
|
if (!theirs) {
|
|
635
2324
|
ctx.stderr.write('usage: git merge <branch>\n');
|
|
636
2325
|
return 1;
|
|
637
2326
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
2327
|
+
// Into the current branch, or HEAD when detached. The branch moves only once the worktree
|
|
2328
|
+
// and index have: a checkout git refuses ("would be overwritten by merge") changes nothing.
|
|
2329
|
+
const ours = await git.currentBranch({ fs, dir, fullname: true }) ?? 'HEAD';
|
|
2330
|
+
const mergeIdents = await commitIdents(ctx, git, fs, dir);
|
|
2331
|
+
if ('error' in mergeIdents) {
|
|
2332
|
+
await ctx.stderr.write(mergeIdents.error);
|
|
2333
|
+
return 128;
|
|
2334
|
+
}
|
|
2335
|
+
const merged = await git.merge({
|
|
2336
|
+
fs, dir, ours, theirs,
|
|
2337
|
+
...mergeIdents,
|
|
2338
|
+
noUpdateBranch: true,
|
|
641
2339
|
});
|
|
642
|
-
|
|
2340
|
+
if (!merged.alreadyMerged) {
|
|
2341
|
+
try {
|
|
2342
|
+
await git.checkout({ fs: createGitFs(credentialedVfs, dir), dir, ref: merged.oid, noUpdateHead: true, conflictOperation: 'merge' });
|
|
2343
|
+
}
|
|
2344
|
+
catch (e) {
|
|
2345
|
+
return await refusal(ctx, e, merged.fastForward ? undefined : 'ort');
|
|
2346
|
+
}
|
|
2347
|
+
await git.writeRef({ fs, dir, ref: ours, value: merged.oid, force: true });
|
|
2348
|
+
}
|
|
2349
|
+
if (!subArgs.includes('-q') && !subArgs.includes('--quiet'))
|
|
2350
|
+
ctx.stdout.write(`Merged ${theirs}\n`);
|
|
643
2351
|
return 0;
|
|
644
2352
|
}
|
|
645
2353
|
case 'reset': {
|
|
@@ -647,13 +2355,16 @@ export async function runGitCommand(ctx, vfs, doCtx, doEnv) {
|
|
|
647
2355
|
const soft = subArgs.includes('--soft');
|
|
648
2356
|
const ref = subArgs.find(a => !a.startsWith('-')) || 'HEAD';
|
|
649
2357
|
const oid = await git.resolveRef({ fs, dir, ref });
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
await git.
|
|
2358
|
+
if (hard) {
|
|
2359
|
+
// The index and worktree become the target's, as a forced checkout from the old index
|
|
2360
|
+
// makes them (type changes included), before the branch moves: a failed write leaves it.
|
|
2361
|
+
await git.checkout({ fs: createGitFs(credentialedVfs, dir), dir, ref: oid, force: true, noUpdateHead: true });
|
|
654
2362
|
}
|
|
655
|
-
|
|
656
|
-
|
|
2363
|
+
// Move the current branch, or a detached HEAD, to the target OID
|
|
2364
|
+
const branch = await git.currentBranch({ fs, dir });
|
|
2365
|
+
await git.writeRef({ fs, dir, ref: branch ? `refs/heads/${branch}` : 'HEAD', value: oid, force: true });
|
|
2366
|
+
if (!hard && !soft) {
|
|
2367
|
+
// Reset index (--mixed)
|
|
657
2368
|
const matrix = await git.statusMatrix({ fs, dir });
|
|
658
2369
|
for (const [filepath] of matrix) {
|
|
659
2370
|
try {
|
|
@@ -662,31 +2373,11 @@ export async function runGitCommand(ctx, vfs, doCtx, doEnv) {
|
|
|
662
2373
|
catch { }
|
|
663
2374
|
}
|
|
664
2375
|
}
|
|
665
|
-
if (hard) {
|
|
666
|
-
// Reset working tree to match the target
|
|
667
|
-
await git.checkout({ fs, dir, ref: oid, force: true });
|
|
668
|
-
}
|
|
669
2376
|
ctx.stdout.write(`HEAD is now at ${oid.slice(0, 7)}\n`);
|
|
670
2377
|
return 0;
|
|
671
2378
|
}
|
|
672
|
-
case 'tag':
|
|
673
|
-
|
|
674
|
-
const tags = await git.listTags({ fs, dir });
|
|
675
|
-
for (const t of tags)
|
|
676
|
-
ctx.stdout.write(t + '\n');
|
|
677
|
-
}
|
|
678
|
-
else if (subArgs.includes('-d')) {
|
|
679
|
-
const name = subArgs.find(a => !a.startsWith('-'));
|
|
680
|
-
if (name)
|
|
681
|
-
await git.deleteTag({ fs, dir, ref: name });
|
|
682
|
-
}
|
|
683
|
-
else {
|
|
684
|
-
const name = subArgs[0];
|
|
685
|
-
await git.tag({ fs, dir, ref: name });
|
|
686
|
-
ctx.stdout.write(`Created tag ${name}\n`);
|
|
687
|
-
}
|
|
688
|
-
return 0;
|
|
689
|
-
}
|
|
2379
|
+
case 'tag':
|
|
2380
|
+
return await tagCommand(ctx, git, fs, credentialedVfs, subArgs);
|
|
690
2381
|
case 'config': {
|
|
691
2382
|
const key = subArgs.find(a => !a.startsWith('-'));
|
|
692
2383
|
const value = subArgs[subArgs.indexOf(key || '') + 1];
|
|
@@ -717,6 +2408,9 @@ export async function runGitCommand(ctx, vfs, doCtx, doEnv) {
|
|
|
717
2408
|
}
|
|
718
2409
|
}
|
|
719
2410
|
catch (e) {
|
|
2411
|
+
// The reader went away (`git diff | head`): git dies of SIGPIPE, silently.
|
|
2412
|
+
if (e?.code === 'EPIPE')
|
|
2413
|
+
return 141;
|
|
720
2414
|
ctx.stderr.write(`fatal: ${e?.message || e}\n`);
|
|
721
2415
|
return 128;
|
|
722
2416
|
}
|