querysub 0.627.0 → 0.628.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/package.json +1 -1
- package/src/4-deploy/git.ts +96 -23
- package/src/storageSetup.ts +1 -0
package/package.json
CHANGED
package/src/4-deploy/git.ts
CHANGED
|
@@ -1,19 +1,92 @@
|
|
|
1
1
|
import * as child_process from "child_process";
|
|
2
2
|
import { cache } from "socket-function/src/caching";
|
|
3
|
-
import { runPromise } from "../functional/runCommand";
|
|
4
3
|
import { measureWrap } from "socket-function/src/profiling/measure";
|
|
4
|
+
import { timeInMinute } from "socket-function/src/misc";
|
|
5
|
+
import { blue, red } from "socket-function/src/formatting/logColors";
|
|
6
|
+
import { formatTime } from "socket-function/src/formatting/format";
|
|
5
7
|
import fs from "fs";
|
|
6
8
|
import os from "os";
|
|
7
9
|
import path from "path";
|
|
10
|
+
|
|
11
|
+
// Resolving on 'close' (as socket-function's runPromise does) waits for the stdio pipes to close, and git background daemons (ex, fsmonitor, which a plain `git status` auto-starts on Windows) inherit those pipe handles and keep them open forever after git itself has exited. So we resolve on 'exit', giving remaining output this long to drain first.
|
|
12
|
+
const OUTPUT_DRAIN_TIME = 100;
|
|
13
|
+
// A hard ceiling so a wedged command (a fetch that never completes, a credential prompt with no console) fails loudly instead of hanging the caller forever. Generous, as clones of large repos legitimately take minutes.
|
|
14
|
+
const COMMAND_TIMEOUT = 10 * timeInMinute;
|
|
15
|
+
|
|
16
|
+
async function runGitCommand(command: string, config?: {
|
|
17
|
+
cwd?: string;
|
|
18
|
+
quiet?: boolean;
|
|
19
|
+
nothrow?: boolean;
|
|
20
|
+
}): Promise<string> {
|
|
21
|
+
if (!config?.quiet) {
|
|
22
|
+
console.log(">" + blue(command));
|
|
23
|
+
}
|
|
24
|
+
return new Promise<string>((resolve, reject) => {
|
|
25
|
+
let childProc = child_process.spawn(command, {
|
|
26
|
+
shell: true,
|
|
27
|
+
cwd: config?.cwd,
|
|
28
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
29
|
+
});
|
|
30
|
+
let fullOutput = "";
|
|
31
|
+
let stderr = "";
|
|
32
|
+
let finished = false;
|
|
33
|
+
function finish(result: { code: number | null } | { error: Error }) {
|
|
34
|
+
if (finished) return;
|
|
35
|
+
finished = true;
|
|
36
|
+
clearTimeout(timeoutHandle);
|
|
37
|
+
if ("error" in result) {
|
|
38
|
+
if (config?.nothrow) {
|
|
39
|
+
resolve(fullOutput);
|
|
40
|
+
} else {
|
|
41
|
+
reject(result.error);
|
|
42
|
+
}
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (result.code === 0 || config?.nothrow) {
|
|
46
|
+
resolve(fullOutput);
|
|
47
|
+
} else {
|
|
48
|
+
let errorMessage = `Process exited with code ${result.code} for command: ${command} (cwd ${path.resolve(config?.cwd || ".")})`;
|
|
49
|
+
if (stderr) {
|
|
50
|
+
errorMessage += `\n${stderr}`;
|
|
51
|
+
}
|
|
52
|
+
reject(new Error(errorMessage));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
let timeoutHandle = setTimeout(() => {
|
|
56
|
+
childProc.kill();
|
|
57
|
+
finish({ error: new Error(`Command timed out after ${formatTime(COMMAND_TIMEOUT)}: ${command} (cwd ${path.resolve(config?.cwd || ".")})\n${fullOutput}`) });
|
|
58
|
+
}, COMMAND_TIMEOUT);
|
|
59
|
+
childProc.stdout?.on("data", data => {
|
|
60
|
+
let chunk = data.toString();
|
|
61
|
+
fullOutput += chunk;
|
|
62
|
+
if (!config?.quiet) {
|
|
63
|
+
process.stdout.write(chunk);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
childProc.stderr?.on("data", data => {
|
|
67
|
+
let chunk = data.toString();
|
|
68
|
+
stderr += chunk;
|
|
69
|
+
fullOutput += chunk;
|
|
70
|
+
if (!config?.quiet) {
|
|
71
|
+
process.stderr.write(red(chunk));
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
childProc.on("error", error => finish({ error }));
|
|
75
|
+
childProc.on("exit", code => {
|
|
76
|
+
setTimeout(() => finish({ code }), OUTPUT_DRAIN_TIME);
|
|
77
|
+
});
|
|
78
|
+
childProc.on("close", code => finish({ code }));
|
|
79
|
+
});
|
|
80
|
+
}
|
|
8
81
|
export async function getGitURLLive(gitDir = ".") {
|
|
9
|
-
return (await
|
|
82
|
+
return (await runGitCommand(`git remote get-url origin`, { cwd: gitDir })).trim();
|
|
10
83
|
}
|
|
11
84
|
export async function getGitRefLive(gitDir = ".") {
|
|
12
|
-
return (await
|
|
85
|
+
return (await runGitCommand(`git rev-parse HEAD`, { cwd: gitDir })).trim();
|
|
13
86
|
}
|
|
14
87
|
|
|
15
88
|
export async function getGitUncommitted(gitDir = "."): Promise<string[]> {
|
|
16
|
-
return (await
|
|
89
|
+
return (await runGitCommand(`git status --porcelain`, { cwd: gitDir })).split("\n").map(x => x.trim()).filter(x => x);
|
|
17
90
|
}
|
|
18
91
|
|
|
19
92
|
// Cap the diff we hand to the client / AI, so a huge accidental change doesn't
|
|
@@ -29,14 +102,14 @@ export async function getGitDiff(gitDir = "."): Promise<string> {
|
|
|
29
102
|
let sections: string[] = [];
|
|
30
103
|
|
|
31
104
|
// Tracked modifications (staged + unstaged) relative to the last commit.
|
|
32
|
-
let trackedDiff = await
|
|
105
|
+
let trackedDiff = await runGitCommand(`git diff HEAD`, { cwd: gitDir, quiet: true });
|
|
33
106
|
if (trackedDiff.trim()) {
|
|
34
107
|
sections.push(trackedDiff);
|
|
35
108
|
}
|
|
36
109
|
|
|
37
110
|
// Untracked files don't show up in `git diff HEAD`, so include their
|
|
38
111
|
// contents as all-addition hunks.
|
|
39
|
-
let untrackedFiles = (await
|
|
112
|
+
let untrackedFiles = (await runGitCommand(`git ls-files --others --exclude-standard`, { cwd: gitDir, quiet: true }))
|
|
40
113
|
.split("\n").map(x => x.trim()).filter(x => x);
|
|
41
114
|
for (let file of untrackedFiles) {
|
|
42
115
|
let content: string;
|
|
@@ -72,8 +145,8 @@ export async function getGitDiff(gitDir = "."): Promise<string> {
|
|
|
72
145
|
}
|
|
73
146
|
|
|
74
147
|
export async function getLatestRefOnUpstreamBranch(gitDir = ".") {
|
|
75
|
-
await
|
|
76
|
-
return (await
|
|
148
|
+
await runGitCommand(`git fetch`, { cwd: gitDir });
|
|
149
|
+
return (await runGitCommand(`git rev-parse @{upstream}`, { cwd: gitDir })).trim();
|
|
77
150
|
}
|
|
78
151
|
|
|
79
152
|
export async function commitAndPush(config: {
|
|
@@ -85,9 +158,9 @@ export async function commitAndPush(config: {
|
|
|
85
158
|
|
|
86
159
|
try {
|
|
87
160
|
await fs.promises.writeFile(tempFile, config.message, "utf8");
|
|
88
|
-
await
|
|
89
|
-
await
|
|
90
|
-
await
|
|
161
|
+
await runGitCommand(`git add --all`, { cwd: config.gitDir });
|
|
162
|
+
await runGitCommand(`git commit -F "${tempFile}"`, { cwd: config.gitDir });
|
|
163
|
+
await runGitCommand(`git push`, { cwd: config.gitDir });
|
|
91
164
|
} finally {
|
|
92
165
|
// Clean up temporary file
|
|
93
166
|
try {
|
|
@@ -105,8 +178,8 @@ export async function getGitRefInfo(config: {
|
|
|
105
178
|
time: number;
|
|
106
179
|
description: string;
|
|
107
180
|
}> {
|
|
108
|
-
const timeOutput = await
|
|
109
|
-
const descriptionOutput = await
|
|
181
|
+
const timeOutput = await runGitCommand(`git show --format="%ct" -s ${config.ref}`, { cwd: config.gitDir });
|
|
182
|
+
const descriptionOutput = await runGitCommand(`git show --format="%B" -s ${config.ref}`, { cwd: config.gitDir });
|
|
110
183
|
|
|
111
184
|
return {
|
|
112
185
|
time: parseInt(timeOutput.trim(), 10) * 1000,
|
|
@@ -128,7 +201,7 @@ export const setGitRef = measureWrap(async function setGitRef(config: {
|
|
|
128
201
|
await fs.promises.mkdir(config.gitFolder, { recursive: true });
|
|
129
202
|
// ssh-keyscan is a network call; a transient failure here must not abort the sync. The host key is normally already in known_hosts from machine setup, and callers only escalate to clobbering the checkout on *repo* failures — not on a momentary inability to refresh a host key.
|
|
130
203
|
try {
|
|
131
|
-
let hostKey = await
|
|
204
|
+
let hostKey = await runGitCommand(`ssh-keyscan -t rsa bitbucket.org`);
|
|
132
205
|
hostKey = hostKey.split("\n").filter(x => !x.startsWith("#")).join("\n");
|
|
133
206
|
let knownHostsPath = os.homedir() + "/.ssh/known_hosts";
|
|
134
207
|
if (hostKey && (!fs.existsSync(knownHostsPath) || !fs.readFileSync(knownHostsPath).toString().includes(hostKey))) {
|
|
@@ -138,14 +211,14 @@ export const setGitRef = measureWrap(async function setGitRef(config: {
|
|
|
138
211
|
console.warn(`ssh-keyscan for bitbucket.org failed, continuing: ${e.stack ?? e}`);
|
|
139
212
|
}
|
|
140
213
|
|
|
141
|
-
await
|
|
142
|
-
await
|
|
214
|
+
await runGitCommand(`git remote update`, { cwd: config.gitFolder });
|
|
215
|
+
await runGitCommand(`git add --all`, { cwd: config.gitFolder });
|
|
143
216
|
// nothrow: on a freshly re-initialized repo (rebuildGitFolder) there is no initial commit yet, so `git stash` errors. That's harmless — the `git reset --hard` below is what actually forces the working tree to the target ref.
|
|
144
|
-
await
|
|
145
|
-
await
|
|
146
|
-
await
|
|
217
|
+
await runGitCommand(`git stash`, { cwd: config.gitFolder, nothrow: true });
|
|
218
|
+
await runGitCommand(`git fetch --all`, { cwd: config.gitFolder });
|
|
219
|
+
await runGitCommand(`git reset --hard ${config.gitRef}`, { cwd: config.gitFolder });
|
|
147
220
|
// Allows us to remove deleted objects from storage, ex, if we accidentally commit a 1GB, this deletes it, so we don't have to fix each server individually. Also I think the repo breaks if we go too long without pruning it?
|
|
148
|
-
await
|
|
221
|
+
await runGitCommand(`git prune`, { cwd: config.gitFolder });
|
|
149
222
|
});
|
|
150
223
|
|
|
151
224
|
/** Recovers a broken repo WITHOUT disturbing the working-tree files. We delete only .git, re-init, re-add the remote, then let setGitRef's `git reset --hard` reconcile the tracked files to the ref. Untracked / gitignored files (notably node_modules, which is expensive to reinstall and may be open in a running service) are left exactly as they are. When the directory is empty this produces the same result as a fresh clone. */
|
|
@@ -156,8 +229,8 @@ export const rebuildGitFolder = measureWrap(async function rebuildGitFolder(conf
|
|
|
156
229
|
}) {
|
|
157
230
|
await fs.promises.mkdir(config.gitFolder, { recursive: true });
|
|
158
231
|
await fs.promises.rm(config.gitFolder + ".git", { recursive: true, force: true });
|
|
159
|
-
await
|
|
160
|
-
await
|
|
232
|
+
await runGitCommand(`git init`, { cwd: config.gitFolder });
|
|
233
|
+
await runGitCommand(`git remote add origin ${config.repoUrl}`, { cwd: config.gitFolder });
|
|
161
234
|
await setGitRef({ gitFolder: config.gitFolder, gitRef: config.gitRef });
|
|
162
235
|
});
|
|
163
236
|
|
|
@@ -170,7 +243,7 @@ export const nuclearReclone = measureWrap(async function nuclearReclone(config:
|
|
|
170
243
|
let target = config.gitFolder.replace(/[\/\\]+$/, "");
|
|
171
244
|
let tempDir = target + ".reclone-" + Date.now();
|
|
172
245
|
await fs.promises.rm(tempDir, { recursive: true, force: true });
|
|
173
|
-
await
|
|
246
|
+
await runGitCommand(`git clone ${config.repoUrl} ${tempDir}`);
|
|
174
247
|
await setGitRef({ gitFolder: tempDir + "/", gitRef: config.gitRef });
|
|
175
248
|
await fs.promises.rm(target, { recursive: true, force: true });
|
|
176
249
|
await fs.promises.rename(tempDir, target);
|
package/src/storageSetup.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
process.env.PRODUCTION = "true";
|
|
2
2
|
import { magenta } from "socket-function/src/formatting/logColors";
|
|
3
3
|
import "./inject";
|
|
4
|
+
process.argv.push("--nobreak");
|
|
4
5
|
|
|
5
6
|
// The storage server entry point (see bin/storageserve.js): sets up the CLI args the sliftutils storage server expects, synchronizes our network trust list into its trust store, and runs its CLI.
|
|
6
7
|
|