@astrosheep/keiyaku 0.1.11 → 0.1.13

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.
@@ -0,0 +1,276 @@
1
+ import { simpleGit } from "simple-git";
2
+ import { appendDebugBlock } from "./debug-log.js";
3
+ const DEFAULT_GIT_TIMEOUT_MS = 15 * 1000;
4
+ function readPositiveIntEnv(name, fallback) {
5
+ const raw = process.env[name]?.trim();
6
+ if (!raw)
7
+ return fallback;
8
+ const value = Number.parseInt(raw, 10);
9
+ return Number.isFinite(value) && value > 0 ? value : fallback;
10
+ }
11
+ function readBooleanEnv(name, fallback) {
12
+ const raw = process.env[name]?.trim().toLowerCase();
13
+ if (!raw)
14
+ return fallback;
15
+ if (raw === "1" || raw === "true" || raw === "yes" || raw === "on")
16
+ return true;
17
+ if (raw === "0" || raw === "false" || raw === "no" || raw === "off")
18
+ return false;
19
+ return fallback;
20
+ }
21
+ function compactText(input, maxChars = 4000) {
22
+ const text = input.trim();
23
+ if (!text)
24
+ return "";
25
+ if (text.length <= maxChars)
26
+ return text;
27
+ const marker = `\n...[truncated ${text.length - maxChars} chars]...\n`;
28
+ const side = Math.floor((maxChars - marker.length) / 2);
29
+ return `${text.slice(0, side)}${marker}${text.slice(text.length - side)}`;
30
+ }
31
+ function wrapGitError(commandLabel, err, cwd) {
32
+ const source = (err ?? {});
33
+ const message = source.message ?? String(err);
34
+ const stderr = compactText(source.stderr ?? source.stdErr ?? "");
35
+ const stdout = compactText(source.stdout ?? source.stdOut ?? "");
36
+ let detailedMessage = `[git ${commandLabel}] ${message}`;
37
+ if (stderr)
38
+ detailedMessage += `\n\n--- GIT STDERR ---\n${stderr}\n------------------`;
39
+ if (stdout)
40
+ detailedMessage += `\n\n--- GIT STDOUT ---\n${stdout}\n------------------`;
41
+ if (stderr || stdout) {
42
+ appendDebugBlock(`git ${commandLabel} failure details`, `${detailedMessage}`, { cwd, section: "git-error" });
43
+ }
44
+ const wrapped = new Error(detailedMessage, err === undefined ? undefined : { cause: err });
45
+ if (source.git !== undefined) {
46
+ Object.assign(wrapped, { git: source.git });
47
+ }
48
+ return wrapped;
49
+ }
50
+ function createGit(cwd) {
51
+ const timeoutMs = readPositiveIntEnv("KEIYAKU_GIT_TIMEOUT_MS", DEFAULT_GIT_TIMEOUT_MS);
52
+ const git = simpleGit(cwd, {
53
+ timeout: { block: timeoutMs, stdErr: true, stdOut: true },
54
+ });
55
+ git.env("GIT_TERMINAL_PROMPT", "0");
56
+ git.env("GCM_INTERACTIVE", "Never");
57
+ git.env("GIT_ASKPASS", "false");
58
+ git.env("SSH_ASKPASS", "false");
59
+ git.env("GIT_EDITOR", "true");
60
+ git.env("GIT_MERGE_AUTOEDIT", "no");
61
+ git.env("LC_ALL", "C");
62
+ return git;
63
+ }
64
+ export async function isGitRepo(cwd) {
65
+ const git = createGit(cwd);
66
+ try {
67
+ return await git.checkIsRepo();
68
+ }
69
+ catch (err) {
70
+ throw wrapGitError("check-is-repo", err, cwd);
71
+ }
72
+ }
73
+ export async function getCurrentBranch(cwd) {
74
+ const git = createGit(cwd);
75
+ let status;
76
+ try {
77
+ status = await git.status();
78
+ }
79
+ catch (err) {
80
+ throw wrapGitError("status", err, cwd);
81
+ }
82
+ if (!status.current) {
83
+ throw new Error("Detached HEAD or no branch found");
84
+ }
85
+ return status.current;
86
+ }
87
+ export async function getActiveKeiyakuBranch(cwd) {
88
+ const current = await getCurrentBranch(cwd);
89
+ return current.startsWith("keiyaku/") ? current : null;
90
+ }
91
+ export async function listLocalKeiyakuBranches(cwd) {
92
+ const git = createGit(cwd);
93
+ let branches;
94
+ try {
95
+ branches = await git.branchLocal();
96
+ }
97
+ catch (err) {
98
+ throw wrapGitError("branch --list", err, cwd);
99
+ }
100
+ return branches.all.filter((name) => name.startsWith("keiyaku/"));
101
+ }
102
+ export async function createAndCheckoutBranch(cwd, branchName) {
103
+ const git = createGit(cwd);
104
+ try {
105
+ await git.checkoutLocalBranch(branchName);
106
+ }
107
+ catch (err) {
108
+ throw wrapGitError(`checkout -b ${branchName}`, err, cwd);
109
+ }
110
+ }
111
+ export async function checkoutBranch(cwd, branchName) {
112
+ const git = createGit(cwd);
113
+ try {
114
+ await git.checkout(branchName);
115
+ }
116
+ catch (err) {
117
+ throw wrapGitError(`checkout ${branchName}`, err, cwd);
118
+ }
119
+ }
120
+ export async function deleteBranch(cwd, branchName, force = false) {
121
+ const git = createGit(cwd);
122
+ try {
123
+ await git.deleteLocalBranch(branchName, force);
124
+ }
125
+ catch (err) {
126
+ throw wrapGitError(`branch -d${force ? " -D" : ""} ${branchName}`, err, cwd);
127
+ }
128
+ }
129
+ export async function addFiles(cwd, files) {
130
+ const git = createGit(cwd);
131
+ try {
132
+ await git.add(files);
133
+ }
134
+ catch (err) {
135
+ throw wrapGitError("add", err, cwd);
136
+ }
137
+ }
138
+ export async function commit(cwd, message) {
139
+ const git = createGit(cwd);
140
+ const skipVerify = readBooleanEnv("KEIYAKU_GIT_NO_VERIFY", true);
141
+ const noGpgSign = readBooleanEnv("KEIYAKU_GIT_NO_GPG_SIGN", true);
142
+ const args = ["commit", "-m", message];
143
+ if (skipVerify)
144
+ args.push("--no-verify");
145
+ if (noGpgSign)
146
+ args.push("--no-gpg-sign");
147
+ try {
148
+ await git.raw(args);
149
+ }
150
+ catch (err) {
151
+ throw wrapGitError(args.join(" "), err, cwd);
152
+ }
153
+ }
154
+ export async function merge(cwd, branchName, message, options = {}) {
155
+ const git = createGit(cwd);
156
+ const { squash = true } = options;
157
+ const skipVerify = readBooleanEnv("KEIYAKU_GIT_NO_VERIFY", true);
158
+ const noGpgSign = readBooleanEnv("KEIYAKU_GIT_NO_GPG_SIGN", true);
159
+ if (!squash) {
160
+ const mergeOptions = [branchName, "--no-ff", "--no-edit", "-m", message];
161
+ if (skipVerify)
162
+ mergeOptions.push("--no-verify");
163
+ if (noGpgSign)
164
+ mergeOptions.push("--no-gpg-sign");
165
+ try {
166
+ await git.merge(mergeOptions);
167
+ return;
168
+ }
169
+ catch (err) {
170
+ throw wrapGitError(`merge ${mergeOptions.join(" ")}`, err, cwd);
171
+ }
172
+ }
173
+ const squashArgs = ["merge", "--squash", branchName];
174
+ try {
175
+ await git.raw(squashArgs);
176
+ }
177
+ catch (err) {
178
+ throw wrapGitError(squashArgs.join(" "), err, cwd);
179
+ }
180
+ const commitArgs = ["commit", "--allow-empty", "-m", message];
181
+ if (skipVerify)
182
+ commitArgs.push("--no-verify");
183
+ if (noGpgSign)
184
+ commitArgs.push("--no-gpg-sign");
185
+ try {
186
+ await git.raw(commitArgs);
187
+ }
188
+ catch (err) {
189
+ throw wrapGitError(commitArgs.join(" "), err, cwd);
190
+ }
191
+ }
192
+ export async function getUnmergedFiles(cwd) {
193
+ const git = createGit(cwd);
194
+ let output;
195
+ try {
196
+ output = await git.raw(["diff", "--name-only", "--diff-filter=U"]);
197
+ }
198
+ catch (err) {
199
+ throw wrapGitError("diff --name-only --diff-filter=U", err, cwd);
200
+ }
201
+ const files = output
202
+ .split(/\r?\n/)
203
+ .map((line) => line.trim())
204
+ .filter((line) => line.length > 0);
205
+ return Array.from(new Set(files));
206
+ }
207
+ export async function hasLocalBranch(cwd, branchName) {
208
+ const git = createGit(cwd);
209
+ let branches;
210
+ try {
211
+ branches = await git.branchLocal();
212
+ }
213
+ catch (err) {
214
+ throw wrapGitError("branch --list", err, cwd);
215
+ }
216
+ return branches.all.includes(branchName);
217
+ }
218
+ export async function getDirtyFiles(cwd) {
219
+ const git = createGit(cwd);
220
+ let status;
221
+ try {
222
+ status = await git.status();
223
+ }
224
+ catch (err) {
225
+ throw wrapGitError("status --porcelain", err, cwd);
226
+ }
227
+ return status.files.map((f) => ({
228
+ path: f.path,
229
+ index: f.index,
230
+ working_dir: f.working_dir,
231
+ }));
232
+ }
233
+ export async function getDirtyPaths(cwd) {
234
+ const files = await getDirtyFiles(cwd);
235
+ return Array.from(new Set(files.map(f => f.path)));
236
+ }
237
+ export async function assertValidBranchName(cwd, branchName) {
238
+ const git = createGit(cwd);
239
+ try {
240
+ await git.raw(["check-ref-format", "--branch", branchName]);
241
+ }
242
+ catch (err) {
243
+ throw wrapGitError(`check-ref-format --branch ${branchName}`, err, cwd);
244
+ }
245
+ }
246
+ function branchBaseKey(branchName) {
247
+ return `branch.${branchName}.keiyakuBase`;
248
+ }
249
+ export async function setKeiyakuBase(cwd, branchName, baseBranch) {
250
+ const git = createGit(cwd);
251
+ try {
252
+ await git.raw(["config", branchBaseKey(branchName), baseBranch]);
253
+ }
254
+ catch (err) {
255
+ throw wrapGitError(`config ${branchBaseKey(branchName)} ${baseBranch}`, err, cwd);
256
+ }
257
+ }
258
+ export async function getKeiyakuBase(cwd, branchName) {
259
+ const git = createGit(cwd);
260
+ try {
261
+ const value = await git.raw(["config", "--get", branchBaseKey(branchName)]);
262
+ return value.trim() || null;
263
+ }
264
+ catch {
265
+ return null;
266
+ }
267
+ }
268
+ export async function clearKeiyakuBase(cwd, branchName) {
269
+ const git = createGit(cwd);
270
+ try {
271
+ await git.raw(["config", "--unset", branchBaseKey(branchName)]);
272
+ }
273
+ catch {
274
+ // Branch config may already be absent.
275
+ }
276
+ }