@cvr/stacked 0.4.2 → 0.4.4
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/README.md +1 -1
- package/bin/stacked +0 -0
- package/package.json +4 -1
- package/scripts/benchmark-detect.ts +308 -0
- package/scripts/benchmark-git.ts +273 -0
- package/src/commands/clean.ts +6 -5
- package/src/commands/create.ts +6 -2
- package/src/commands/delete.ts +1 -1
- package/src/commands/detect.ts +64 -32
- package/src/commands/doctor.ts +14 -8
- package/src/commands/helpers/pr-metadata.ts +131 -0
- package/src/commands/list.ts +3 -4
- package/src/commands/rename.ts +5 -9
- package/src/commands/reorder.ts +11 -17
- package/src/commands/split.ts +4 -28
- package/src/commands/stacks.ts +3 -5
- package/src/commands/submit.ts +15 -87
- package/src/commands/sync.ts +33 -13
- package/src/main.ts +31 -29
- package/src/services/Git.ts +20 -0
- package/src/services/GitEs.ts +309 -0
- package/src/services/Stack.ts +621 -182
- package/src/services/git-backend.ts +18 -0
package/src/commands/sync.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Command, Flag } from "effect/unstable/cli";
|
|
2
2
|
import { Console, Effect, Option } from "effect";
|
|
3
3
|
import { GitService } from "../services/Git.js";
|
|
4
|
+
import { GitHubService } from "../services/GitHub.js";
|
|
4
5
|
import { StackService } from "../services/Stack.js";
|
|
5
6
|
import { ErrorCode, StackError } from "../errors/index.js";
|
|
7
|
+
import { refreshStackedPRBodies } from "./helpers/pr-metadata.js";
|
|
6
8
|
import { withSpinner, success, warn } from "../ui.js";
|
|
7
9
|
|
|
8
10
|
const trunkFlag = Flag.string("trunk").pipe(
|
|
@@ -43,6 +45,7 @@ export const sync = Command.make("sync", {
|
|
|
43
45
|
Command.withHandler(({ trunk: trunkOpt, from: fromOpt, json, dryRun }) =>
|
|
44
46
|
Effect.gen(function* () {
|
|
45
47
|
const git = yield* GitService;
|
|
48
|
+
const gh = yield* GitHubService;
|
|
46
49
|
const stacks = yield* StackService;
|
|
47
50
|
|
|
48
51
|
const trunk = Option.isSome(trunkOpt) ? trunkOpt.value : yield* stacks.getTrunk();
|
|
@@ -116,21 +119,21 @@ export const sync = Command.make("sync", {
|
|
|
116
119
|
return;
|
|
117
120
|
}
|
|
118
121
|
|
|
119
|
-
yield*
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
Effect.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
122
|
+
yield* Effect.gen(function* () {
|
|
123
|
+
yield* withSpinner(`Fetching ${trunk}`, git.fetch());
|
|
124
|
+
yield* git.checkout(trunk);
|
|
125
|
+
yield* withSpinner(`Rebasing ${trunk} onto ${originTrunk}`, git.rebase(originTrunk)).pipe(
|
|
126
|
+
Effect.catchTag("GitError", (e) =>
|
|
127
|
+
Effect.fail(
|
|
128
|
+
new StackError({
|
|
129
|
+
code: ErrorCode.REBASE_CONFLICT,
|
|
130
|
+
message: `Rebase conflict on ${trunk}: ${e.message}\n\nResolve conflicts, then run:\n git rebase --continue`,
|
|
131
|
+
}),
|
|
132
|
+
),
|
|
128
133
|
),
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
results.push({ name: trunk, action: "rebased", base: originTrunk });
|
|
134
|
+
);
|
|
135
|
+
results.push({ name: trunk, action: "rebased", base: originTrunk });
|
|
132
136
|
|
|
133
|
-
yield* Effect.gen(function* () {
|
|
134
137
|
for (let i = startIdx; i < branches.length; i++) {
|
|
135
138
|
const branch = branches[i];
|
|
136
139
|
if (branch === undefined) continue;
|
|
@@ -172,6 +175,23 @@ export const sync = Command.make("sync", {
|
|
|
172
175
|
),
|
|
173
176
|
);
|
|
174
177
|
|
|
178
|
+
const ghInstalled = yield* gh.isGhInstalled();
|
|
179
|
+
if (ghInstalled) {
|
|
180
|
+
const prMap = yield* refreshStackedPRBodies({
|
|
181
|
+
branches,
|
|
182
|
+
stackName: result.name,
|
|
183
|
+
gh,
|
|
184
|
+
});
|
|
185
|
+
const mergedBranches = branches.filter(
|
|
186
|
+
(branch) => (prMap.get(branch)?.state ?? "") === "MERGED",
|
|
187
|
+
);
|
|
188
|
+
const activeBranches = branches.filter(
|
|
189
|
+
(branch) => (prMap.get(branch)?.state ?? "") !== "MERGED",
|
|
190
|
+
);
|
|
191
|
+
yield* stacks.markMergedBranches(mergedBranches);
|
|
192
|
+
yield* stacks.unmarkMergedBranches(activeBranches);
|
|
193
|
+
}
|
|
194
|
+
|
|
175
195
|
if (json) {
|
|
176
196
|
// @effect-diagnostics-next-line effect/preferSchemaOverJson:off
|
|
177
197
|
yield* Console.log(JSON.stringify({ branches: results }, null, 2));
|
package/src/main.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { Command } from "effect/unstable/cli";
|
|
|
3
3
|
import { BunRuntime, BunServices } from "@effect/platform-bun";
|
|
4
4
|
import { Console, Effect, Layer } from "effect";
|
|
5
5
|
import { command } from "./commands/index.js";
|
|
6
|
-
import {
|
|
6
|
+
import { gitBackendConfig, gitServiceLayerForBackend } from "./services/git-backend.js";
|
|
7
7
|
import { StackService } from "./services/Stack.js";
|
|
8
8
|
import { GitHubService } from "./services/GitHub.js";
|
|
9
9
|
import { OutputConfig } from "./ui.js";
|
|
@@ -41,13 +41,6 @@ const cli = Command.run(command, {
|
|
|
41
41
|
version,
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
const ServiceLayer = StackService.layer.pipe(
|
|
45
|
-
Layer.provideMerge(GitService.layer),
|
|
46
|
-
Layer.provideMerge(GitHubService.layer),
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
const AppLayer = Layer.mergeAll(ServiceLayer, BunServices.layer);
|
|
50
|
-
|
|
51
44
|
// Usage errors (bad args, invalid state) → exit 2
|
|
52
45
|
// Operational errors (git/gh failures) → exit 1
|
|
53
46
|
const usageCodes = new Set([
|
|
@@ -73,25 +66,34 @@ const handleKnownError = (e: { message: string; code?: string | undefined }) =>
|
|
|
73
66
|
|
|
74
67
|
// @effect-diagnostics-next-line effect/strictEffectProvide:off
|
|
75
68
|
BunRuntime.runMain(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
(
|
|
87
|
-
Effect.
|
|
88
|
-
(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
69
|
+
Effect.gen(function* () {
|
|
70
|
+
const gitBackend = yield* gitBackendConfig;
|
|
71
|
+
const serviceLayer = StackService.layer.pipe(
|
|
72
|
+
Layer.provideMerge(gitServiceLayerForBackend(gitBackend)),
|
|
73
|
+
Layer.provideMerge(GitHubService.layer),
|
|
74
|
+
);
|
|
75
|
+
const appLayer = Layer.mergeAll(serviceLayer, BunServices.layer);
|
|
76
|
+
|
|
77
|
+
yield* preflight.pipe(
|
|
78
|
+
Effect.andThen(cli),
|
|
79
|
+
Effect.provideService(OutputConfig, { verbose: isVerbose, quiet: isQuiet, yes: isYes }),
|
|
80
|
+
Effect.provide(appLayer),
|
|
81
|
+
Effect.catchTags({
|
|
82
|
+
GitError: (e) => handleKnownError(e),
|
|
83
|
+
StackError: (e) => handleKnownError(e),
|
|
84
|
+
GitHubError: (e) => handleKnownError(e),
|
|
85
|
+
}),
|
|
86
|
+
Effect.catchIf(
|
|
87
|
+
(e): e is GlobalFlagConflictError => e instanceof GlobalFlagConflictError,
|
|
88
|
+
Effect.fail,
|
|
89
|
+
(e) => {
|
|
90
|
+
const msg =
|
|
91
|
+
e !== null && typeof e === "object" && "message" in e
|
|
92
|
+
? String(e.message)
|
|
93
|
+
: JSON.stringify(e, null, 2);
|
|
94
|
+
return handleKnownError({ message: `Unexpected error: ${msg}` });
|
|
95
|
+
},
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
}),
|
|
97
99
|
);
|
package/src/services/Git.ts
CHANGED
|
@@ -28,6 +28,11 @@ export class GitService extends ServiceMap.Service<
|
|
|
28
28
|
readonly revParse: (ref: string) => Effect.Effect<string, GitError>;
|
|
29
29
|
readonly isAncestor: (ancestor: string, descendant: string) => Effect.Effect<boolean, GitError>;
|
|
30
30
|
readonly mergeBase: (a: string, b: string) => Effect.Effect<string, GitError>;
|
|
31
|
+
readonly firstParentUniqueCommits: (
|
|
32
|
+
ref: string,
|
|
33
|
+
base: string,
|
|
34
|
+
options?: { limit?: number },
|
|
35
|
+
) => Effect.Effect<readonly string[], GitError>;
|
|
31
36
|
readonly isRebaseInProgress: () => Effect.Effect<boolean>;
|
|
32
37
|
readonly commitAmend: (options?: { edit?: boolean }) => Effect.Effect<void, GitError>;
|
|
33
38
|
readonly fetch: (remote?: string) => Effect.Effect<void, GitError>;
|
|
@@ -163,6 +168,20 @@ export class GitService extends ServiceMap.Service<
|
|
|
163
168
|
|
|
164
169
|
mergeBase: (a, b) => run(["merge-base", a, b]),
|
|
165
170
|
|
|
171
|
+
firstParentUniqueCommits: (ref, base, options) => {
|
|
172
|
+
const args = ["rev-list", "--first-parent"];
|
|
173
|
+
if (options?.limit !== undefined) args.push("--max-count", `${options.limit}`);
|
|
174
|
+
args.push(ref, `^${base}`);
|
|
175
|
+
return run(args).pipe(
|
|
176
|
+
Effect.map((output) =>
|
|
177
|
+
output
|
|
178
|
+
.split("\n")
|
|
179
|
+
.map((line) => line.trim())
|
|
180
|
+
.filter((line) => line.length > 0),
|
|
181
|
+
),
|
|
182
|
+
);
|
|
183
|
+
},
|
|
184
|
+
|
|
166
185
|
isRebaseInProgress: () =>
|
|
167
186
|
run(["rev-parse", "--git-dir"]).pipe(
|
|
168
187
|
Effect.map(
|
|
@@ -203,6 +222,7 @@ export class GitService extends ServiceMap.Service<
|
|
|
203
222
|
revParse: () => Effect.succeed("abc123"),
|
|
204
223
|
isAncestor: () => Effect.succeed(true),
|
|
205
224
|
mergeBase: () => Effect.succeed("abc123"),
|
|
225
|
+
firstParentUniqueCommits: () => Effect.succeed([]),
|
|
206
226
|
isRebaseInProgress: () => Effect.succeed(false),
|
|
207
227
|
commitAmend: () => Effect.void,
|
|
208
228
|
fetch: () => Effect.void,
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { Effect, Layer, Option } from "effect";
|
|
3
|
+
import { openRepository, type Repository, type SignaturePayload } from "es-git";
|
|
4
|
+
import { GitError } from "../errors/index.js";
|
|
5
|
+
import { GitService } from "./Git.js";
|
|
6
|
+
|
|
7
|
+
const makeGitError = (command: string, error: unknown) =>
|
|
8
|
+
new GitError({
|
|
9
|
+
message: error instanceof Error ? error.message : String(error),
|
|
10
|
+
command,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const resolveSignature = (repo: Repository): SignaturePayload => {
|
|
14
|
+
const config = repo.config();
|
|
15
|
+
const name =
|
|
16
|
+
process.env["GIT_COMMITTER_NAME"] ??
|
|
17
|
+
process.env["GIT_AUTHOR_NAME"] ??
|
|
18
|
+
config.findString("user.name");
|
|
19
|
+
const email =
|
|
20
|
+
process.env["GIT_COMMITTER_EMAIL"] ??
|
|
21
|
+
process.env["GIT_AUTHOR_EMAIL"] ??
|
|
22
|
+
config.findString("user.email");
|
|
23
|
+
|
|
24
|
+
if (name === null || name === undefined || email === null || email === undefined) {
|
|
25
|
+
throw makeGitError("es-git.signature", "Missing git user.name or user.email");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return { name, email };
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const refName = (name: string) => (name.startsWith("refs/") ? name : `refs/heads/${name}`);
|
|
32
|
+
|
|
33
|
+
const resolveOid = (repo: Repository, ref: string): string => {
|
|
34
|
+
if (ref === "--absolute-git-dir" || ref === "--git-dir") {
|
|
35
|
+
return repo.path();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return repo.revparseSingle(ref);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const performRebase = (
|
|
42
|
+
repo: Repository,
|
|
43
|
+
branchRef: string,
|
|
44
|
+
upstreamRef: string,
|
|
45
|
+
ontoRef: string,
|
|
46
|
+
): void => {
|
|
47
|
+
const branch = repo.getAnnotatedCommitFromReference(repo.getReference(branchRef));
|
|
48
|
+
const upstream = repo.getAnnotatedCommit(repo.getCommit(resolveOid(repo, upstreamRef)));
|
|
49
|
+
const onto = repo.getAnnotatedCommit(repo.getCommit(resolveOid(repo, ontoRef)));
|
|
50
|
+
const signature = resolveSignature(repo);
|
|
51
|
+
const rebase = repo.rebase(branch, upstream, onto);
|
|
52
|
+
while (true) {
|
|
53
|
+
const operation = rebase.next();
|
|
54
|
+
if (operation === null) break;
|
|
55
|
+
rebase.commit({ committer: signature });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
rebase.finish(signature);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const GitEsLayer = Layer.effect(
|
|
62
|
+
GitService,
|
|
63
|
+
Effect.gen(function* () {
|
|
64
|
+
const cwd = process.cwd();
|
|
65
|
+
const repo = yield* Effect.tryPromise({
|
|
66
|
+
try: () => openRepository(cwd),
|
|
67
|
+
catch: (error) => makeGitError(`es-git.openRepository ${cwd}`, error),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
currentBranch: () =>
|
|
72
|
+
Effect.try({
|
|
73
|
+
try: () => {
|
|
74
|
+
const branch = repo.head().shorthand();
|
|
75
|
+
if (branch === "HEAD") {
|
|
76
|
+
throw makeGitError(
|
|
77
|
+
"es-git.currentBranch",
|
|
78
|
+
"HEAD is detached — checkout a branch first",
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return branch;
|
|
82
|
+
},
|
|
83
|
+
catch: (error) => makeGitError("es-git.currentBranch", error),
|
|
84
|
+
}),
|
|
85
|
+
|
|
86
|
+
listBranches: () =>
|
|
87
|
+
Effect.try({
|
|
88
|
+
try: () =>
|
|
89
|
+
[...repo.branches({ type: "Local" })]
|
|
90
|
+
.map(({ name }) => ({
|
|
91
|
+
name,
|
|
92
|
+
commitTime: repo
|
|
93
|
+
.getCommit(resolveOid(repo, refName(name)))
|
|
94
|
+
.time()
|
|
95
|
+
.getTime(),
|
|
96
|
+
}))
|
|
97
|
+
.sort((a, b) => b.commitTime - a.commitTime)
|
|
98
|
+
.map(({ name }) => name),
|
|
99
|
+
catch: (error) => makeGitError("es-git.listBranches", error),
|
|
100
|
+
}),
|
|
101
|
+
|
|
102
|
+
branchExists: (name) =>
|
|
103
|
+
Effect.try({
|
|
104
|
+
try: () => repo.findBranch(name, "Local") !== null,
|
|
105
|
+
catch: (error) => makeGitError(`es-git.branchExists ${name}`, error),
|
|
106
|
+
}),
|
|
107
|
+
|
|
108
|
+
remoteDefaultBranch: (remote = "origin") =>
|
|
109
|
+
Effect.try({
|
|
110
|
+
try: () => {
|
|
111
|
+
const ref = repo.findReference(`refs/remotes/${remote}/HEAD`);
|
|
112
|
+
if (ref === null) return Option.none();
|
|
113
|
+
const target = ref.resolve().shorthand();
|
|
114
|
+
return Option.some(target.replace(new RegExp(`^${remote}/`), ""));
|
|
115
|
+
},
|
|
116
|
+
catch: (error) => makeGitError(`es-git.remoteDefaultBranch ${remote}`, error),
|
|
117
|
+
}).pipe(Effect.catchTag("GitError", () => Effect.succeed(Option.none()))),
|
|
118
|
+
|
|
119
|
+
createBranch: (name, from) =>
|
|
120
|
+
Effect.try({
|
|
121
|
+
try: () => {
|
|
122
|
+
const commit = repo.getCommit(resolveOid(repo, from ?? "HEAD"));
|
|
123
|
+
repo.createBranch(name, commit);
|
|
124
|
+
repo.setHead(refName(name));
|
|
125
|
+
repo.checkoutHead();
|
|
126
|
+
},
|
|
127
|
+
catch: (error) => makeGitError(`es-git.createBranch ${name}`, error),
|
|
128
|
+
}).pipe(Effect.asVoid),
|
|
129
|
+
|
|
130
|
+
deleteBranch: (name) =>
|
|
131
|
+
Effect.try({
|
|
132
|
+
try: () => {
|
|
133
|
+
repo.getBranch(name, "Local").delete();
|
|
134
|
+
},
|
|
135
|
+
catch: (error) => makeGitError(`es-git.deleteBranch ${name}`, error),
|
|
136
|
+
}).pipe(Effect.asVoid),
|
|
137
|
+
|
|
138
|
+
checkout: (name) =>
|
|
139
|
+
Effect.try({
|
|
140
|
+
try: () => {
|
|
141
|
+
repo.setHead(refName(name));
|
|
142
|
+
repo.checkoutHead();
|
|
143
|
+
},
|
|
144
|
+
catch: (error) => makeGitError(`es-git.checkout ${name}`, error),
|
|
145
|
+
}).pipe(Effect.asVoid),
|
|
146
|
+
|
|
147
|
+
rebase: (onto) =>
|
|
148
|
+
Effect.try({
|
|
149
|
+
try: () => {
|
|
150
|
+
const headRef = repo.head().resolve();
|
|
151
|
+
const headOid = headRef.target();
|
|
152
|
+
if (headOid === null) {
|
|
153
|
+
throw makeGitError(`es-git.rebase ${onto}`, "HEAD has no target");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const ontoOid = resolveOid(repo, onto);
|
|
157
|
+
const upstreamOid = repo.getMergeBase(headOid, ontoOid);
|
|
158
|
+
performRebase(repo, headRef.name(), upstreamOid, onto);
|
|
159
|
+
},
|
|
160
|
+
catch: (error) => makeGitError(`es-git.rebase ${onto}`, error),
|
|
161
|
+
}).pipe(Effect.asVoid),
|
|
162
|
+
|
|
163
|
+
rebaseOnto: (branch, newBase, oldBase) =>
|
|
164
|
+
Effect.try({
|
|
165
|
+
try: () => {
|
|
166
|
+
performRebase(repo, refName(branch), oldBase, newBase);
|
|
167
|
+
},
|
|
168
|
+
catch: (error) =>
|
|
169
|
+
makeGitError(`es-git.rebaseOnto ${branch} ${newBase} ${oldBase}`, error),
|
|
170
|
+
}).pipe(Effect.asVoid),
|
|
171
|
+
|
|
172
|
+
rebaseAbort: () =>
|
|
173
|
+
Effect.try({
|
|
174
|
+
try: () => {
|
|
175
|
+
repo.openRebase().abort();
|
|
176
|
+
},
|
|
177
|
+
catch: (error) => makeGitError("es-git.rebaseAbort", error),
|
|
178
|
+
}).pipe(Effect.asVoid),
|
|
179
|
+
|
|
180
|
+
push: (branch, options) =>
|
|
181
|
+
Effect.tryPromise({
|
|
182
|
+
try: async () => {
|
|
183
|
+
const remote = repo.getRemote("origin");
|
|
184
|
+
const forcePrefix = options?.force === true ? "+" : "";
|
|
185
|
+
await remote.push([`${forcePrefix}refs/heads/${branch}:refs/heads/${branch}`]);
|
|
186
|
+
const localBranch = repo.findBranch(branch, "Local");
|
|
187
|
+
localBranch?.setUpstream(`origin/${branch}`);
|
|
188
|
+
},
|
|
189
|
+
catch: (error) => makeGitError(`es-git.push ${branch}`, error),
|
|
190
|
+
}).pipe(Effect.asVoid),
|
|
191
|
+
|
|
192
|
+
log: (branch, options) =>
|
|
193
|
+
Effect.try({
|
|
194
|
+
try: () => {
|
|
195
|
+
const revwalk = repo.revwalk();
|
|
196
|
+
revwalk.push(resolveOid(repo, branch));
|
|
197
|
+
|
|
198
|
+
const lines: string[] = [];
|
|
199
|
+
while (lines.length < (options?.limit ?? Number.POSITIVE_INFINITY)) {
|
|
200
|
+
const oid = revwalk.next();
|
|
201
|
+
if (oid === null) break;
|
|
202
|
+
|
|
203
|
+
if (options?.oneline === true) {
|
|
204
|
+
const summary = repo.getCommit(oid).summary() ?? "";
|
|
205
|
+
lines.push(`${oid.slice(0, 7)} ${summary}`.trim());
|
|
206
|
+
} else {
|
|
207
|
+
lines.push(oid);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return lines.join("\n");
|
|
212
|
+
},
|
|
213
|
+
catch: (error) => makeGitError(`es-git.log ${branch}`, error),
|
|
214
|
+
}),
|
|
215
|
+
|
|
216
|
+
isClean: () =>
|
|
217
|
+
Effect.try({
|
|
218
|
+
try: () => repo.statuses().isEmpty(),
|
|
219
|
+
catch: (error) => makeGitError("es-git.isClean", error),
|
|
220
|
+
}),
|
|
221
|
+
|
|
222
|
+
revParse: (ref) =>
|
|
223
|
+
Effect.try({
|
|
224
|
+
try: () => resolveOid(repo, ref),
|
|
225
|
+
catch: (error) => makeGitError(`es-git.revParse ${ref}`, error),
|
|
226
|
+
}),
|
|
227
|
+
|
|
228
|
+
isAncestor: (ancestor, descendant) =>
|
|
229
|
+
Effect.try({
|
|
230
|
+
try: () =>
|
|
231
|
+
repo.getMergeBase(resolveOid(repo, ancestor), resolveOid(repo, descendant)) ===
|
|
232
|
+
resolveOid(repo, ancestor),
|
|
233
|
+
catch: (error) => makeGitError(`es-git.isAncestor ${ancestor} ${descendant}`, error),
|
|
234
|
+
}).pipe(Effect.catchTag("GitError", () => Effect.succeed(false))),
|
|
235
|
+
|
|
236
|
+
mergeBase: (a, b) =>
|
|
237
|
+
Effect.try({
|
|
238
|
+
try: () => repo.getMergeBase(resolveOid(repo, a), resolveOid(repo, b)),
|
|
239
|
+
catch: (error) => makeGitError(`es-git.mergeBase ${a} ${b}`, error),
|
|
240
|
+
}),
|
|
241
|
+
|
|
242
|
+
firstParentUniqueCommits: (ref, base, options) =>
|
|
243
|
+
Effect.try({
|
|
244
|
+
try: () => {
|
|
245
|
+
const revwalk = repo.revwalk();
|
|
246
|
+
revwalk.simplifyFirstParent();
|
|
247
|
+
revwalk.push(resolveOid(repo, ref));
|
|
248
|
+
revwalk.hide(resolveOid(repo, base));
|
|
249
|
+
|
|
250
|
+
const commits: string[] = [];
|
|
251
|
+
const limit = options?.limit ?? Number.POSITIVE_INFINITY;
|
|
252
|
+
while (commits.length < limit) {
|
|
253
|
+
const oid = revwalk.next();
|
|
254
|
+
if (oid === null) break;
|
|
255
|
+
commits.push(oid);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return commits;
|
|
259
|
+
},
|
|
260
|
+
catch: (error) => makeGitError(`es-git.firstParentUniqueCommits ${ref} ${base}`, error),
|
|
261
|
+
}),
|
|
262
|
+
|
|
263
|
+
isRebaseInProgress: () =>
|
|
264
|
+
Effect.try({
|
|
265
|
+
try: () => {
|
|
266
|
+
const state = repo.state();
|
|
267
|
+
if (state === "Rebase" || state === "RebaseInteractive" || state === "RebaseMerge") {
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const gitDir = repo.path();
|
|
272
|
+
return existsSync(`${gitDir}/rebase-merge`) || existsSync(`${gitDir}/rebase-apply`);
|
|
273
|
+
},
|
|
274
|
+
catch: (error) => makeGitError("es-git.isRebaseInProgress", error),
|
|
275
|
+
}).pipe(Effect.catchTag("GitError", () => Effect.succeed(false))),
|
|
276
|
+
|
|
277
|
+
commitAmend: () =>
|
|
278
|
+
Effect.try({
|
|
279
|
+
try: () => {
|
|
280
|
+
const headOid = repo.head().target();
|
|
281
|
+
if (headOid === null) {
|
|
282
|
+
throw makeGitError("es-git.commitAmend", "HEAD has no target");
|
|
283
|
+
}
|
|
284
|
+
repo.getCommit(headOid).amend({
|
|
285
|
+
updateRef: "HEAD",
|
|
286
|
+
committer: resolveSignature(repo),
|
|
287
|
+
});
|
|
288
|
+
},
|
|
289
|
+
catch: (error) => makeGitError("es-git.commitAmend", error),
|
|
290
|
+
}).pipe(Effect.asVoid),
|
|
291
|
+
|
|
292
|
+
fetch: (remote = "origin") =>
|
|
293
|
+
Effect.tryPromise({
|
|
294
|
+
try: async () => {
|
|
295
|
+
await repo.getRemote(remote).fetch([]);
|
|
296
|
+
},
|
|
297
|
+
catch: (error) => makeGitError(`es-git.fetch ${remote}`, error),
|
|
298
|
+
}).pipe(Effect.asVoid),
|
|
299
|
+
|
|
300
|
+
deleteRemoteBranch: (branch) =>
|
|
301
|
+
Effect.tryPromise({
|
|
302
|
+
try: async () => {
|
|
303
|
+
await repo.getRemote("origin").push([`:refs/heads/${branch}`]);
|
|
304
|
+
},
|
|
305
|
+
catch: (error) => makeGitError(`es-git.deleteRemoteBranch ${branch}`, error),
|
|
306
|
+
}).pipe(Effect.asVoid),
|
|
307
|
+
};
|
|
308
|
+
}),
|
|
309
|
+
);
|