@nimbus-sh/worker 0.10.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/facets/cirrus-real.d.ts.map +1 -1
- package/dist/facets/cirrus-real.js +9 -4
- package/dist/facets/compose.d.ts +11 -7
- package/dist/facets/compose.d.ts.map +1 -1
- package/dist/facets/compose.js +8 -38
- 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-transform.d.ts +36 -10
- package/dist/facets/esbuild-transform.d.ts.map +1 -1
- package/dist/facets/esbuild-transform.js +216 -20
- 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 +179 -78
- package/dist/facets/manager.d.ts.map +1 -1
- package/dist/facets/manager.js +1221 -649
- 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/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 +16 -9
- package/dist/git/commands.d.ts.map +1 -1
- package/dist/git/commands.js +1818 -188
- package/dist/git/network-facet.d.ts +10 -2
- package/dist/git/network-facet.d.ts.map +1 -1
- package/dist/git/network-facet.js +62 -16
- 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 +62 -243
- package/dist/hosted/runtime.d.ts +19 -13
- package/dist/hosted/runtime.d.ts.map +1 -1
- package/dist/hosted/runtime.js +71 -14
- package/dist/hosted/services.d.ts +6 -7
- package/dist/hosted/services.d.ts.map +1 -1
- package/dist/hosted/services.js +10 -14
- 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 +5 -2
- package/dist/loaders/generated-workers.d.ts.map +1 -1
- package/dist/loaders/generated-workers.js +6 -3
- package/dist/loaders/npm-resolve-preamble.d.ts.map +1 -1
- package/dist/loaders/npm-resolve-preamble.js +0 -27
- 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 +5 -0
- package/dist/npm/cache.d.ts.map +1 -1
- package/dist/npm/cache.js +13 -1
- package/dist/npm/install-batch-facet.d.ts +2 -2
- package/dist/npm/install-batch-facet.js +3 -3
- package/dist/npm/installer.d.ts +17 -1
- package/dist/npm/installer.d.ts.map +1 -1
- package/dist/npm/installer.js +159 -20
- 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/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 +2 -0
- package/dist/npm/r2-cache.d.ts.map +1 -1
- package/dist/npm/r2-cache.js +2 -0
- package/dist/npm/resolve-one-facet.d.ts.map +1 -1
- package/dist/npm/resolve-one-facet.js +14 -3
- package/dist/npm/resolver.d.ts +0 -7
- package/dist/npm/resolver.d.ts.map +1 -1
- package/dist/npm/resolver.js +0 -22
- 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/esbuild-wasm-bytes.d.ts +17 -5
- package/dist/runtime/esbuild-wasm-bytes.d.ts.map +1 -1
- package/dist/runtime/esbuild-wasm-bytes.js +43 -94
- 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 +1567 -864
- 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 +2 -2
- 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 +36 -35
- 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.map +1 -1
- package/dist/runtime/wasi/preamble.js +0 -24
- 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 +34 -11
- package/dist/session/hibernation.d.ts.map +1 -1
- package/dist/session/hibernation.js +46 -29
- 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 +41 -15
- package/dist/session/nimbus-session.d.ts.map +1 -1
- package/dist/session/nimbus-session.js +64 -70
- package/dist/session/npm-install-port.d.ts.map +1 -1
- package/dist/session/npm-install-port.js +1 -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 +7 -10
- package/dist/session/rpc.d.ts +104 -20
- package/dist/session/rpc.d.ts.map +1 -1
- package/dist/session/rpc.js +184 -37
- 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 +104 -15
- package/dist/session/supervisor-rpc.d.ts.map +1 -1
- package/dist/session/supervisor-rpc.js +188 -71
- 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 +2 -0
- 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 +5 -22
- package/package.json +8 -7
- 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-7b4dd7a76d95d21e.js → node-shims-06565b121555c194.js} +2089 -861
- 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-facet-workers.mjs +114 -3
- 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,21 +48,90 @@ 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);
|
|
62
99
|
}
|
|
63
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
|
+
}
|
|
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
|
+
};
|
|
134
|
+
}
|
|
64
135
|
return {
|
|
65
136
|
promises: {
|
|
66
137
|
async readFile(filepath, opts) {
|
|
@@ -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,73 +180,40 @@ 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
|
}
|
|
@@ -280,11 +324,1576 @@ export function parseCloneArgs(args) {
|
|
|
280
324
|
quiet,
|
|
281
325
|
};
|
|
282
326
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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;
|
|
287
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;
|
|
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;
|
|
288
1897
|
}
|
|
289
1898
|
// ── Git subcommand implementations ──────────────────────────────────────
|
|
290
1899
|
/**
|
|
@@ -316,8 +1925,8 @@ export async function runGitCommand(ctx, vfs, doCtx, doEnv) {
|
|
|
316
1925
|
ctx.stdout.write('usage: git <command> [<args>]\n\n');
|
|
317
1926
|
ctx.stdout.write('Commands:\n');
|
|
318
1927
|
ctx.stdout.write(' init, clone, status, add, commit, log, branch,\n');
|
|
319
|
-
ctx.stdout.write(' checkout, diff,
|
|
320
|
-
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');
|
|
321
1930
|
return 0;
|
|
322
1931
|
}
|
|
323
1932
|
// Lazy-load isomorphic-git only when actually needed.
|
|
@@ -345,7 +1954,9 @@ export async function runGitCommand(ctx, vfs, doCtx, doEnv) {
|
|
|
345
1954
|
credentialedVfs.mkdir(stripped, { recursive: true });
|
|
346
1955
|
}
|
|
347
1956
|
await git.init({ fs, dir: initDir });
|
|
348
|
-
|
|
1957
|
+
if (!subArgs.includes('-q') && !subArgs.includes('--quiet')) {
|
|
1958
|
+
ctx.stdout.write(`Initialized empty Git repository in ${initDir}/.git/\n`);
|
|
1959
|
+
}
|
|
349
1960
|
return 0;
|
|
350
1961
|
}
|
|
351
1962
|
case 'clone': {
|
|
@@ -457,41 +2068,47 @@ export async function runGitCommand(ctx, vfs, doCtx, doEnv) {
|
|
|
457
2068
|
ctx.stdout.write('nothing to commit, working tree clean\n');
|
|
458
2069
|
return 0;
|
|
459
2070
|
}
|
|
460
|
-
case 'add':
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
await git.add({ fs, dir, filepath });
|
|
471
|
-
}
|
|
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;
|
|
472
2081
|
}
|
|
2082
|
+
catch { /* unset */ }
|
|
473
2083
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}
|
|
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;
|
|
478
2089
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const message = msgIdx >= 0 ? subArgs[msgIdx + 1] : 'commit';
|
|
484
|
-
if (!message) {
|
|
485
|
-
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');
|
|
486
2094
|
return 1;
|
|
487
2095
|
}
|
|
488
|
-
|
|
489
|
-
fs, dir
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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`);
|
|
493
2106
|
return 0;
|
|
494
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);
|
|
495
2112
|
case 'log': {
|
|
496
2113
|
const maxCount = parseInt(getFlag(subArgs, '-n') || getFlag(subArgs, '--max-count') || '10');
|
|
497
2114
|
const oneline = subArgs.includes('--oneline');
|
|
@@ -552,47 +2169,38 @@ export async function runGitCommand(ctx, vfs, doCtx, doEnv) {
|
|
|
552
2169
|
return 0;
|
|
553
2170
|
}
|
|
554
2171
|
case 'checkout': {
|
|
555
|
-
|
|
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;
|
|
556
2184
|
if (!ref) {
|
|
557
|
-
ctx.stderr.write(
|
|
558
|
-
return
|
|
2185
|
+
ctx.stderr.write("error: switch `b' requires a value\n");
|
|
2186
|
+
return 129;
|
|
559
2187
|
}
|
|
560
|
-
|
|
2188
|
+
const worktreeFs = createGitFs(credentialedVfs, dir);
|
|
2189
|
+
const create = options.includes('-b');
|
|
2190
|
+
if (create)
|
|
561
2191
|
await git.branch({ fs, dir, ref });
|
|
562
|
-
|
|
563
|
-
|
|
2192
|
+
try {
|
|
2193
|
+
await git.checkout({ fs: worktreeFs, dir, ref });
|
|
564
2194
|
}
|
|
565
|
-
|
|
566
|
-
await
|
|
567
|
-
ctx.stdout.write(`Switched to branch '${ref}'\n`);
|
|
568
|
-
}
|
|
569
|
-
return 0;
|
|
570
|
-
}
|
|
571
|
-
case 'diff': {
|
|
572
|
-
// Simple diff: show unstaged changes
|
|
573
|
-
const matrix = await git.statusMatrix({ fs, dir });
|
|
574
|
-
for (const [filepath, head, workdir, stage] of matrix) {
|
|
575
|
-
if (workdir !== head || workdir !== stage) {
|
|
576
|
-
ctx.stdout.write(`\x1b[1mdiff --git a/${filepath} b/${filepath}\x1b[0m\n`);
|
|
577
|
-
try {
|
|
578
|
-
const raw = await fs.promises.readFile(dir + '/' + filepath);
|
|
579
|
-
const content = typeof raw === 'string' ? raw : dec.decode(raw);
|
|
580
|
-
const lines = content.split('\n');
|
|
581
|
-
for (let i = 0; i < Math.min(lines.length, 50); i++) {
|
|
582
|
-
if (head === 0)
|
|
583
|
-
ctx.stdout.write(`\x1b[32m+${lines[i]}\x1b[0m\n`);
|
|
584
|
-
else
|
|
585
|
-
ctx.stdout.write(` ${lines[i]}\n`);
|
|
586
|
-
}
|
|
587
|
-
if (lines.length > 50)
|
|
588
|
-
ctx.stdout.write(`... (${lines.length - 50} more lines)\n`);
|
|
589
|
-
}
|
|
590
|
-
catch { }
|
|
591
|
-
ctx.stdout.write('\n');
|
|
592
|
-
}
|
|
2195
|
+
catch (e) {
|
|
2196
|
+
return await refusal(ctx, e);
|
|
593
2197
|
}
|
|
2198
|
+
if (!quiet)
|
|
2199
|
+
ctx.stdout.write(create ? `Switched to a new branch '${ref}'\n` : `Switched to branch '${ref}'\n`);
|
|
594
2200
|
return 0;
|
|
595
2201
|
}
|
|
2202
|
+
case 'diff':
|
|
2203
|
+
return await diffCommand(ctx, git, fs, credentialedVfs, subArgs);
|
|
596
2204
|
case 'remote': {
|
|
597
2205
|
if (subArgs[0] === 'add' && subArgs[1] && subArgs[2]) {
|
|
598
2206
|
await git.addRemote({ fs, dir, remote: subArgs[1], url: subArgs[2] });
|
|
@@ -611,24 +2219,28 @@ export async function runGitCommand(ctx, vfs, doCtx, doEnv) {
|
|
|
611
2219
|
return 0;
|
|
612
2220
|
}
|
|
613
2221
|
case 'fetch': {
|
|
614
|
-
const
|
|
2222
|
+
const { quiet, rest } = takeQuiet(subArgs);
|
|
2223
|
+
const remote = rest[0] || 'origin';
|
|
615
2224
|
if (!doCtx || !doEnv) {
|
|
616
2225
|
ctx.stderr.write('[git] fetch requires DO ctx + env (internal configuration error)\n');
|
|
617
2226
|
return 1;
|
|
618
2227
|
}
|
|
619
|
-
|
|
2228
|
+
if (!quiet)
|
|
2229
|
+
ctx.stdout.write(`Fetching from ${remote}...\n`);
|
|
620
2230
|
const result = await execGitNetwork(doCtx, doEnv, {
|
|
621
2231
|
op: 'fetch',
|
|
622
2232
|
pid: ctx.pid,
|
|
623
2233
|
dir,
|
|
624
2234
|
remote,
|
|
2235
|
+
quiet,
|
|
625
2236
|
auth: {
|
|
626
2237
|
username: ctx.env.GIT_USERNAME || '',
|
|
627
2238
|
password: ctx.env.GIT_PASSWORD || ctx.env.GIT_TOKEN || '',
|
|
628
2239
|
},
|
|
629
2240
|
});
|
|
630
2241
|
if (result.success) {
|
|
631
|
-
|
|
2242
|
+
if (!quiet)
|
|
2243
|
+
ctx.stdout.write(`\n[git] fetch complete (${result.filesWritten} files in ${(result.elapsed / 1000).toFixed(1)}s)\n`);
|
|
632
2244
|
return 0;
|
|
633
2245
|
}
|
|
634
2246
|
else {
|
|
@@ -637,27 +2249,36 @@ export async function runGitCommand(ctx, vfs, doCtx, doEnv) {
|
|
|
637
2249
|
}
|
|
638
2250
|
}
|
|
639
2251
|
case 'pull': {
|
|
640
|
-
const
|
|
641
|
-
const
|
|
2252
|
+
const { quiet, rest } = takeQuiet(subArgs);
|
|
2253
|
+
const remote = rest[0] || 'origin';
|
|
2254
|
+
const branch = rest[1] || await git.currentBranch({ fs, dir }) || 'main';
|
|
642
2255
|
if (!doCtx || !doEnv) {
|
|
643
2256
|
ctx.stderr.write('[git] pull requires DO ctx + env (internal configuration error)\n');
|
|
644
2257
|
return 1;
|
|
645
2258
|
}
|
|
646
|
-
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`);
|
|
647
2266
|
const result = await execGitNetwork(doCtx, doEnv, {
|
|
648
2267
|
op: 'pull',
|
|
649
2268
|
pid: ctx.pid,
|
|
650
2269
|
dir,
|
|
651
2270
|
remote,
|
|
652
2271
|
ref: branch,
|
|
653
|
-
|
|
2272
|
+
quiet,
|
|
2273
|
+
...pullIdents,
|
|
654
2274
|
auth: {
|
|
655
2275
|
username: ctx.env.GIT_USERNAME || '',
|
|
656
2276
|
password: ctx.env.GIT_PASSWORD || ctx.env.GIT_TOKEN || '',
|
|
657
2277
|
},
|
|
658
2278
|
});
|
|
659
2279
|
if (result.success) {
|
|
660
|
-
|
|
2280
|
+
if (!quiet)
|
|
2281
|
+
ctx.stdout.write(`\n[git] pull complete (${result.filesWritten} files in ${(result.elapsed / 1000).toFixed(1)}s)\n`);
|
|
661
2282
|
return 0;
|
|
662
2283
|
}
|
|
663
2284
|
else {
|
|
@@ -666,26 +2287,30 @@ export async function runGitCommand(ctx, vfs, doCtx, doEnv) {
|
|
|
666
2287
|
}
|
|
667
2288
|
}
|
|
668
2289
|
case 'push': {
|
|
669
|
-
const
|
|
670
|
-
const
|
|
2290
|
+
const { quiet, rest } = takeQuiet(subArgs);
|
|
2291
|
+
const remote = rest[0] || 'origin';
|
|
2292
|
+
const branch = rest[1] || await git.currentBranch({ fs, dir }) || 'main';
|
|
671
2293
|
if (!doCtx || !doEnv) {
|
|
672
2294
|
ctx.stderr.write('[git] push requires DO ctx + env (internal configuration error)\n');
|
|
673
2295
|
return 1;
|
|
674
2296
|
}
|
|
675
|
-
|
|
2297
|
+
if (!quiet)
|
|
2298
|
+
ctx.stdout.write(`Pushing to ${remote}/${branch}...\n`);
|
|
676
2299
|
const result = await execGitNetwork(doCtx, doEnv, {
|
|
677
2300
|
op: 'push',
|
|
678
2301
|
pid: ctx.pid,
|
|
679
2302
|
dir,
|
|
680
2303
|
remote,
|
|
681
2304
|
ref: branch,
|
|
2305
|
+
quiet,
|
|
682
2306
|
auth: {
|
|
683
2307
|
username: ctx.env.GIT_USERNAME || '',
|
|
684
2308
|
password: ctx.env.GIT_PASSWORD || ctx.env.GIT_TOKEN || '',
|
|
685
2309
|
},
|
|
686
2310
|
});
|
|
687
2311
|
if (result.success) {
|
|
688
|
-
|
|
2312
|
+
if (!quiet)
|
|
2313
|
+
ctx.stdout.write(`\n[git] push complete (${(result.elapsed / 1000).toFixed(1)}s)\n`);
|
|
689
2314
|
return 0;
|
|
690
2315
|
}
|
|
691
2316
|
else {
|
|
@@ -694,16 +2319,35 @@ export async function runGitCommand(ctx, vfs, doCtx, doEnv) {
|
|
|
694
2319
|
}
|
|
695
2320
|
}
|
|
696
2321
|
case 'merge': {
|
|
697
|
-
const theirs = subArgs
|
|
2322
|
+
const theirs = subArgs.find(a => !a.startsWith('-'));
|
|
698
2323
|
if (!theirs) {
|
|
699
2324
|
ctx.stderr.write('usage: git merge <branch>\n');
|
|
700
2325
|
return 1;
|
|
701
2326
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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,
|
|
705
2339
|
});
|
|
706
|
-
|
|
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`);
|
|
707
2351
|
return 0;
|
|
708
2352
|
}
|
|
709
2353
|
case 'reset': {
|
|
@@ -711,13 +2355,16 @@ export async function runGitCommand(ctx, vfs, doCtx, doEnv) {
|
|
|
711
2355
|
const soft = subArgs.includes('--soft');
|
|
712
2356
|
const ref = subArgs.find(a => !a.startsWith('-')) || 'HEAD';
|
|
713
2357
|
const oid = await git.resolveRef({ fs, dir, ref });
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
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 });
|
|
718
2362
|
}
|
|
719
|
-
|
|
720
|
-
|
|
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)
|
|
721
2368
|
const matrix = await git.statusMatrix({ fs, dir });
|
|
722
2369
|
for (const [filepath] of matrix) {
|
|
723
2370
|
try {
|
|
@@ -726,31 +2373,11 @@ export async function runGitCommand(ctx, vfs, doCtx, doEnv) {
|
|
|
726
2373
|
catch { }
|
|
727
2374
|
}
|
|
728
2375
|
}
|
|
729
|
-
if (hard) {
|
|
730
|
-
// Reset working tree to match the target
|
|
731
|
-
await git.checkout({ fs, dir, ref: oid, force: true });
|
|
732
|
-
}
|
|
733
2376
|
ctx.stdout.write(`HEAD is now at ${oid.slice(0, 7)}\n`);
|
|
734
2377
|
return 0;
|
|
735
2378
|
}
|
|
736
|
-
case 'tag':
|
|
737
|
-
|
|
738
|
-
const tags = await git.listTags({ fs, dir });
|
|
739
|
-
for (const t of tags)
|
|
740
|
-
ctx.stdout.write(t + '\n');
|
|
741
|
-
}
|
|
742
|
-
else if (subArgs.includes('-d')) {
|
|
743
|
-
const name = subArgs.find(a => !a.startsWith('-'));
|
|
744
|
-
if (name)
|
|
745
|
-
await git.deleteTag({ fs, dir, ref: name });
|
|
746
|
-
}
|
|
747
|
-
else {
|
|
748
|
-
const name = subArgs[0];
|
|
749
|
-
await git.tag({ fs, dir, ref: name });
|
|
750
|
-
ctx.stdout.write(`Created tag ${name}\n`);
|
|
751
|
-
}
|
|
752
|
-
return 0;
|
|
753
|
-
}
|
|
2379
|
+
case 'tag':
|
|
2380
|
+
return await tagCommand(ctx, git, fs, credentialedVfs, subArgs);
|
|
754
2381
|
case 'config': {
|
|
755
2382
|
const key = subArgs.find(a => !a.startsWith('-'));
|
|
756
2383
|
const value = subArgs[subArgs.indexOf(key || '') + 1];
|
|
@@ -781,6 +2408,9 @@ export async function runGitCommand(ctx, vfs, doCtx, doEnv) {
|
|
|
781
2408
|
}
|
|
782
2409
|
}
|
|
783
2410
|
catch (e) {
|
|
2411
|
+
// The reader went away (`git diff | head`): git dies of SIGPIPE, silently.
|
|
2412
|
+
if (e?.code === 'EPIPE')
|
|
2413
|
+
return 141;
|
|
784
2414
|
ctx.stderr.write(`fatal: ${e?.message || e}\n`);
|
|
785
2415
|
return 128;
|
|
786
2416
|
}
|