@markjaquith/agency 2.71.9 → 2.71.10
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markjaquith/agency",
|
|
3
|
-
"version": "2.71.
|
|
3
|
+
"version": "2.71.10",
|
|
4
4
|
"description": "Manage agentic work across repositories with durable workbases",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
"benchmark:doctor": "bun scripts/benchmark-doctor.ts",
|
|
71
71
|
"benchmark:context": "bun scripts/benchmark-context.ts",
|
|
72
72
|
"benchmark:finish": "bun scripts/benchmark-finish.ts",
|
|
73
|
+
"benchmark:push": "bun scripts/benchmark-push.ts",
|
|
73
74
|
"benchmark:sync": "bun scripts/benchmark-sync.ts",
|
|
74
75
|
"benchmark:task": "bun scripts/benchmark-task.ts",
|
|
75
76
|
"benchmark:worktree": "bun scripts/benchmark-worktree.ts",
|
|
@@ -5,6 +5,7 @@ import { join } from "node:path"
|
|
|
5
5
|
import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
|
|
6
6
|
import { TaskService } from "./TaskService"
|
|
7
7
|
import { PushService } from "./PushService"
|
|
8
|
+
import { parseGitCommits } from "./push-validation"
|
|
8
9
|
import { WorktreeService } from "./WorktreeService"
|
|
9
10
|
|
|
10
11
|
interface CommandResult {
|
|
@@ -39,6 +40,24 @@ const requireCommand = async (args: readonly string[], cwd?: string) => {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
describe("PushService", () => {
|
|
43
|
+
test("parses and validates batched Git commit metadata", () => {
|
|
44
|
+
const output = [
|
|
45
|
+
"abc123\0Agency Test\0agency@example.com\0First change\0\x1e",
|
|
46
|
+
"def456\0Agency Test\0agency@example.com\0Second change\0\x1e",
|
|
47
|
+
].join("\n")
|
|
48
|
+
const commits = parseGitCommits(output)
|
|
49
|
+
|
|
50
|
+
expect(
|
|
51
|
+
commits.map(({ commitId, description }) => ({ commitId, description })),
|
|
52
|
+
).toEqual([
|
|
53
|
+
{ commitId: "abc123", description: "First change" },
|
|
54
|
+
{ commitId: "def456", description: "Second change" },
|
|
55
|
+
])
|
|
56
|
+
expect(commits.every((commit) => commit.authorEmail.includes("@"))).toBe(
|
|
57
|
+
true,
|
|
58
|
+
)
|
|
59
|
+
})
|
|
60
|
+
|
|
42
61
|
const roots: string[] = []
|
|
43
62
|
|
|
44
63
|
afterEach(async () => {
|
|
@@ -4,6 +4,7 @@ import { FileSystemService } from "./FileSystemService"
|
|
|
4
4
|
import { PhaseService } from "./PhaseService"
|
|
5
5
|
import { TaskService } from "./TaskService"
|
|
6
6
|
import { WorkbaseService } from "./WorkbaseService"
|
|
7
|
+
import { parseGitCommits, type PushCommitMetadata } from "./push-validation"
|
|
7
8
|
|
|
8
9
|
class PushError extends Data.TaggedError("PushError")<{
|
|
9
10
|
readonly message: string
|
|
@@ -15,16 +16,7 @@ interface CommandResult {
|
|
|
15
16
|
readonly stderr: string
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
readonly commitId: string
|
|
20
|
-
readonly changeId?: string
|
|
21
|
-
readonly description: string
|
|
22
|
-
readonly empty: boolean
|
|
23
|
-
readonly authorName: string
|
|
24
|
-
readonly authorEmail: string
|
|
25
|
-
readonly conflict: boolean
|
|
26
|
-
readonly parents: readonly string[]
|
|
27
|
-
}
|
|
19
|
+
type CommitMetadata = PushCommitMetadata
|
|
28
20
|
|
|
29
21
|
interface PushResult {
|
|
30
22
|
readonly vcs: "git" | "jj"
|
|
@@ -105,29 +97,6 @@ const gitAncestor = (
|
|
|
105
97
|
}),
|
|
106
98
|
)
|
|
107
99
|
|
|
108
|
-
const parseGitCommits = (output: string): readonly CommitMetadata[] =>
|
|
109
|
-
output
|
|
110
|
-
.split("\x1e")
|
|
111
|
-
.map((record) => record.replace(/^\n+|\n+$/g, ""))
|
|
112
|
-
.filter(Boolean)
|
|
113
|
-
.map((record) => {
|
|
114
|
-
const [
|
|
115
|
-
commitId = "",
|
|
116
|
-
authorName = "",
|
|
117
|
-
authorEmail = "",
|
|
118
|
-
description = "",
|
|
119
|
-
] = record.split("\0")
|
|
120
|
-
return {
|
|
121
|
-
commitId,
|
|
122
|
-
description,
|
|
123
|
-
empty: false,
|
|
124
|
-
authorName,
|
|
125
|
-
authorEmail,
|
|
126
|
-
conflict: false,
|
|
127
|
-
parents: [],
|
|
128
|
-
}
|
|
129
|
-
})
|
|
130
|
-
|
|
131
100
|
const validateGitCommits = (
|
|
132
101
|
commits: readonly CommitMetadata[],
|
|
133
102
|
base: string,
|
|
@@ -194,11 +163,12 @@ const publishGit = (
|
|
|
194
163
|
["fetch", remote, `+refs/heads/*:refs/remotes/${remote}/*`],
|
|
195
164
|
`Failed to fetch remote '${remote}'`,
|
|
196
165
|
)
|
|
197
|
-
const tip = yield*
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
166
|
+
const [tip, baseRevision] = yield* Effect.all(
|
|
167
|
+
[
|
|
168
|
+
gitRevision(fs, checkout, "HEAD"),
|
|
169
|
+
gitRevision(fs, checkout, `refs/remotes/${remote}/${base}`),
|
|
170
|
+
],
|
|
171
|
+
{ concurrency: "unbounded" },
|
|
202
172
|
)
|
|
203
173
|
if (!tip || !baseRevision) {
|
|
204
174
|
return yield* new PushError({
|
|
@@ -239,12 +209,6 @@ const publishGit = (
|
|
|
239
209
|
}
|
|
240
210
|
|
|
241
211
|
onProgress?.("publish")
|
|
242
|
-
yield* git(
|
|
243
|
-
fs,
|
|
244
|
-
checkout,
|
|
245
|
-
["push", remote, `HEAD:refs/heads/${branch}`],
|
|
246
|
-
`Failed to push declared branch '${branch}'`,
|
|
247
|
-
)
|
|
248
212
|
yield* git(
|
|
249
213
|
fs,
|
|
250
214
|
checkout,
|
|
@@ -259,17 +223,13 @@ const publishGit = (
|
|
|
259
223
|
fs,
|
|
260
224
|
checkout,
|
|
261
225
|
[
|
|
262
|
-
"
|
|
226
|
+
"push",
|
|
227
|
+
"-u",
|
|
263
228
|
remote,
|
|
264
|
-
|
|
229
|
+
`HEAD:refs/heads/${branch}`,
|
|
230
|
+
`--force-if-includes`,
|
|
265
231
|
],
|
|
266
|
-
`Failed to
|
|
267
|
-
)
|
|
268
|
-
yield* git(
|
|
269
|
-
fs,
|
|
270
|
-
checkout,
|
|
271
|
-
["branch", "--set-upstream-to", `${remote}/${branch}`, branch],
|
|
272
|
-
`Failed to establish upstream tracking for branch '${branch}'`,
|
|
232
|
+
`Failed to push declared branch '${branch}'`,
|
|
273
233
|
)
|
|
274
234
|
return { tip }
|
|
275
235
|
})
|
|
@@ -403,10 +363,25 @@ const publishJj = (
|
|
|
403
363
|
}
|
|
404
364
|
|
|
405
365
|
const baseBookmark = `${base}@${remote}`
|
|
406
|
-
const baseRevision = yield*
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
366
|
+
const [baseRevision, localBookmark, remoteBookmark] = yield* Effect.all(
|
|
367
|
+
[
|
|
368
|
+
optionalJjRevision(
|
|
369
|
+
fs,
|
|
370
|
+
checkout,
|
|
371
|
+
`remote_bookmarks(exact:"${jjExact(base)}", exact:"${jjExact(remote)}")`,
|
|
372
|
+
),
|
|
373
|
+
optionalJjRevision(
|
|
374
|
+
fs,
|
|
375
|
+
checkout,
|
|
376
|
+
`bookmarks(exact:"${jjExact(branch)}")`,
|
|
377
|
+
),
|
|
378
|
+
optionalJjRevision(
|
|
379
|
+
fs,
|
|
380
|
+
checkout,
|
|
381
|
+
`remote_bookmarks(exact:"${jjExact(branch)}", exact:"${jjExact(remote)}")`,
|
|
382
|
+
),
|
|
383
|
+
],
|
|
384
|
+
{ concurrency: "unbounded" },
|
|
410
385
|
)
|
|
411
386
|
if (!baseRevision) {
|
|
412
387
|
return yield* new PushError({
|
|
@@ -432,28 +407,24 @@ const publishJj = (
|
|
|
432
407
|
catch: (cause) => cause as PushError,
|
|
433
408
|
})
|
|
434
409
|
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
410
|
+
const [localBookmarkIsAncestor, remoteBookmarkIsAncestor] =
|
|
411
|
+
yield* Effect.all(
|
|
412
|
+
[
|
|
413
|
+
localBookmark
|
|
414
|
+
? jjAncestor(fs, checkout, localBookmark.commitId, tip.commitId)
|
|
415
|
+
: Effect.succeed(true),
|
|
416
|
+
remoteBookmark
|
|
417
|
+
? jjAncestor(fs, checkout, remoteBookmark.commitId, tip.commitId)
|
|
418
|
+
: Effect.succeed(true),
|
|
419
|
+
],
|
|
420
|
+
{ concurrency: "unbounded" },
|
|
421
|
+
)
|
|
422
|
+
if (!localBookmarkIsAncestor) {
|
|
444
423
|
return yield* new PushError({
|
|
445
424
|
message: `Local bookmark '${branch}' is not an ancestor of jj tip ${tip.changeId}; refusing to move it`,
|
|
446
425
|
})
|
|
447
426
|
}
|
|
448
|
-
|
|
449
|
-
fs,
|
|
450
|
-
checkout,
|
|
451
|
-
`remote_bookmarks(exact:"${jjExact(branch)}", exact:"${jjExact(remote)}")`,
|
|
452
|
-
)
|
|
453
|
-
if (
|
|
454
|
-
remoteBookmark &&
|
|
455
|
-
!(yield* jjAncestor(fs, checkout, remoteBookmark.commitId, tip.commitId))
|
|
456
|
-
) {
|
|
427
|
+
if (!remoteBookmarkIsAncestor) {
|
|
457
428
|
return yield* new PushError({
|
|
458
429
|
message: `Remote bookmark '${branch}@${remote}' is not an ancestor of jj tip ${tip.changeId}; refusing a non-fast-forward update`,
|
|
459
430
|
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface PushCommitMetadata {
|
|
2
|
+
readonly commitId: string
|
|
3
|
+
readonly changeId?: string
|
|
4
|
+
readonly description: string
|
|
5
|
+
readonly empty: boolean
|
|
6
|
+
readonly authorName: string
|
|
7
|
+
readonly authorEmail: string
|
|
8
|
+
readonly conflict: boolean
|
|
9
|
+
readonly parents: readonly string[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const parseGitCommits = (
|
|
13
|
+
output: string,
|
|
14
|
+
): readonly PushCommitMetadata[] =>
|
|
15
|
+
output
|
|
16
|
+
.split("\x1e")
|
|
17
|
+
.map((record) => record.replace(/^\n+|\n+$/g, ""))
|
|
18
|
+
.filter(Boolean)
|
|
19
|
+
.map((record) => {
|
|
20
|
+
const [
|
|
21
|
+
commitId = "",
|
|
22
|
+
authorName = "",
|
|
23
|
+
authorEmail = "",
|
|
24
|
+
description = "",
|
|
25
|
+
] = record.split("\0")
|
|
26
|
+
return {
|
|
27
|
+
commitId,
|
|
28
|
+
description,
|
|
29
|
+
empty: false,
|
|
30
|
+
authorName,
|
|
31
|
+
authorEmail,
|
|
32
|
+
conflict: false,
|
|
33
|
+
parents: [],
|
|
34
|
+
}
|
|
35
|
+
})
|