jj-octo 0.1.0 → 0.2.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/README.md +6 -2
- package/dist/jj-octo.js +281 -179
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,12 +27,14 @@ version = "X.Y.Z"
|
|
|
27
27
|
allow_low_downloads = true
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
Then
|
|
30
|
+
Then set up each checkout, since jj doesn't read config from a committed file:
|
|
31
31
|
|
|
32
32
|
```
|
|
33
|
-
jj
|
|
33
|
+
jj-octo init
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
It makes the checkout a jj repository colocated with git, makes GitHub's default branch `trunk()`, and sets the `jj gh` alias.
|
|
37
|
+
|
|
36
38
|
### For every repository
|
|
37
39
|
|
|
38
40
|
```
|
|
@@ -40,6 +42,8 @@ bun install --global jj-octo
|
|
|
40
42
|
jj config set --user aliases.gh '["util", "exec", "--", "jj-octo"]'
|
|
41
43
|
```
|
|
42
44
|
|
|
45
|
+
Then run `jj-octo init` once in each checkout, as above.
|
|
46
|
+
|
|
43
47
|
To update it later:
|
|
44
48
|
|
|
45
49
|
```
|
package/dist/jj-octo.js
CHANGED
|
@@ -18116,7 +18116,7 @@ var layer14 = /* @__PURE__ */ layerMergedContext(/* @__PURE__ */ succeed6(fetch)
|
|
|
18116
18116
|
// package.json
|
|
18117
18117
|
var package_default = {
|
|
18118
18118
|
name: "jj-octo",
|
|
18119
|
-
version: "0.
|
|
18119
|
+
version: "0.2.0",
|
|
18120
18120
|
description: "A jj subcommand for working with GitHub that supports stacking",
|
|
18121
18121
|
license: "MIT",
|
|
18122
18122
|
repository: {
|
|
@@ -18169,117 +18169,61 @@ var withHelp = (summary, ...paragraphs) => (command) => command.pipe(withShortDe
|
|
|
18169
18169
|
|
|
18170
18170
|
`)));
|
|
18171
18171
|
var revisionFlag = (description) => String8("revision").pipe(withAlias2("r"), withDefault5("@"), withDescription2(`${description}, the working copy by default`));
|
|
18172
|
-
// src/clients/
|
|
18173
|
-
|
|
18174
|
-
var bodyOf = (body) => bodyJsonUnsafe(body);
|
|
18175
|
-
|
|
18176
|
-
// src/clients/github/types.ts
|
|
18177
|
-
var PullRequestNumberSchema = Number6.pipe(brand2("PullRequestNumber"));
|
|
18178
|
-
var StackNumberSchema = Number6.pipe(brand2("StackNumber"));
|
|
18179
|
-
|
|
18180
|
-
// src/clients/github/client.ts
|
|
18181
|
-
class GitHubFailed extends TaggedError2("GitHubFailed") {
|
|
18172
|
+
// src/clients/git.ts
|
|
18173
|
+
class GitFailed extends TaggedError2("GitFailed") {
|
|
18182
18174
|
}
|
|
18183
18175
|
|
|
18184
|
-
class
|
|
18176
|
+
class Git extends Service()("jj-octo/Git") {
|
|
18185
18177
|
}
|
|
18186
|
-
var
|
|
18187
|
-
var CreatedStack = responseOf()(Struct({ number: StackNumberSchema }));
|
|
18188
|
-
var PullRequests = responseOf()(ArraySchema(Struct({
|
|
18189
|
-
base: Struct({ ref: String4 }),
|
|
18190
|
-
body: NullOr(String4),
|
|
18191
|
-
draft: optionalKey2(Boolean2),
|
|
18192
|
-
number: PullRequestNumberSchema,
|
|
18193
|
-
title: String4
|
|
18194
|
-
})));
|
|
18195
|
-
var StackedPullRequest = Struct({
|
|
18196
|
-
number: PullRequestNumberSchema,
|
|
18197
|
-
state: Literals(["closed", "open"])
|
|
18198
|
-
});
|
|
18199
|
-
var Stacks = responseOf()(ArraySchema(Struct({ number: StackNumberSchema, pull_requests: ArraySchema(StackedPullRequest) })));
|
|
18200
|
-
var RepositoryDetails = responseOf()(Struct({ default_branch: String4 }));
|
|
18201
|
-
var MergeRequest = Struct({
|
|
18202
|
-
details: Struct({
|
|
18203
|
-
message: String4,
|
|
18204
|
-
uuid: optionalKey2(String4)
|
|
18205
|
-
}),
|
|
18206
|
-
status: Literals(["enqueued", "failed", "merged", "pending"])
|
|
18207
|
-
});
|
|
18208
|
-
var StartedMerge = responseOf()(MergeRequest);
|
|
18209
|
-
var MergeResult = responseOf()(MergeRequest);
|
|
18210
|
-
var token = gen2(function* () {
|
|
18211
|
-
const fromEnvironment = yield* option2(Redacted2("GH_TOKEN").pipe(orElse(() => Redacted2("GITHUB_TOKEN"))));
|
|
18212
|
-
if (isSome2(fromEnvironment)) {
|
|
18213
|
-
return fromEnvironment.value;
|
|
18214
|
-
}
|
|
18178
|
+
var gitLayer = ({ cwd, env } = {}) => effect(Git, gen2(function* () {
|
|
18215
18179
|
const spawner = yield* ChildProcessSpawner;
|
|
18216
|
-
const
|
|
18217
|
-
|
|
18218
|
-
|
|
18219
|
-
|
|
18220
|
-
|
|
18221
|
-
|
|
18222
|
-
|
|
18223
|
-
|
|
18224
|
-
|
|
18225
|
-
|
|
18226
|
-
|
|
18227
|
-
|
|
18228
|
-
|
|
18229
|
-
|
|
18230
|
-
|
|
18231
|
-
|
|
18232
|
-
|
|
18233
|
-
|
|
18234
|
-
|
|
18235
|
-
|
|
18236
|
-
|
|
18237
|
-
|
|
18238
|
-
|
|
18239
|
-
|
|
18240
|
-
|
|
18241
|
-
|
|
18242
|
-
|
|
18243
|
-
|
|
18244
|
-
|
|
18245
|
-
|
|
18246
|
-
|
|
18247
|
-
|
|
18248
|
-
|
|
18249
|
-
|
|
18250
|
-
|
|
18251
|
-
|
|
18252
|
-
|
|
18253
|
-
|
|
18254
|
-
|
|
18255
|
-
|
|
18256
|
-
|
|
18257
|
-
}
|
|
18258
|
-
|
|
18259
|
-
|
|
18260
|
-
|
|
18261
|
-
|
|
18262
|
-
|
|
18263
|
-
return settled.status;
|
|
18264
|
-
}
|
|
18265
|
-
return yield* new GitHubFailed({
|
|
18266
|
-
message: `merging #${pullRequest} ended ${settled.status}: ${settled.details.message}`
|
|
18267
|
-
});
|
|
18268
|
-
}),
|
|
18269
|
-
openPullRequest: (branch) => json(PullRequests)(get4(repo(`/pulls?state=open&head=${repository.owner}:${encodeURIComponent(branch)}`))).pipe(map7((found) => map(fromNullishOr(found[0]), ({ base, body, draft, number, title }) => ({
|
|
18270
|
-
base: base.ref,
|
|
18271
|
-
body: body ?? "",
|
|
18272
|
-
draft: draft ?? false,
|
|
18273
|
-
number,
|
|
18274
|
-
title
|
|
18275
|
-
})))),
|
|
18276
|
-
setBase: (pullRequest, base) => send(patch(repo(`/pulls/${pullRequest}`)).pipe(bodyOf({ base }))).pipe(asVoid2),
|
|
18277
|
-
setDescription: (pullRequest, { body, title }) => send(patch(repo(`/pulls/${pullRequest}`)).pipe(bodyOf({ body, title }))).pipe(asVoid2),
|
|
18278
|
-
stackOf,
|
|
18279
|
-
unstack: (stack) => send(post(repo(`/stacks/${stack}/unstack`))).pipe(asVoid2)
|
|
18280
|
-
};
|
|
18281
|
-
}));
|
|
18282
|
-
};
|
|
18180
|
+
const fs = yield* FileSystem;
|
|
18181
|
+
const path = yield* Path;
|
|
18182
|
+
return {
|
|
18183
|
+
mergeText: ({ base, local, remote }) => scoped2(gen2(function* () {
|
|
18184
|
+
const directory = yield* fs.makeTempDirectoryScoped();
|
|
18185
|
+
const written = { base: `${base}
|
|
18186
|
+
`, github: `${remote}
|
|
18187
|
+
`, local: `${local}
|
|
18188
|
+
` };
|
|
18189
|
+
yield* all2(Object.entries(written).map(([name, text]) => fs.writeFileString(path.join(directory, name), text)));
|
|
18190
|
+
const handle = yield* spawner.spawn(make14("git", [
|
|
18191
|
+
"merge-file",
|
|
18192
|
+
"-p",
|
|
18193
|
+
"-L",
|
|
18194
|
+
"local",
|
|
18195
|
+
"-L",
|
|
18196
|
+
"base",
|
|
18197
|
+
"-L",
|
|
18198
|
+
"GitHub",
|
|
18199
|
+
"local",
|
|
18200
|
+
"base",
|
|
18201
|
+
"github"
|
|
18202
|
+
], { cwd: directory }));
|
|
18203
|
+
const [text, stderr, code] = yield* all2([
|
|
18204
|
+
mkString(decodeText(handle.stdout)),
|
|
18205
|
+
mkString(decodeText(handle.stderr)),
|
|
18206
|
+
handle.exitCode
|
|
18207
|
+
], { concurrency: "unbounded" });
|
|
18208
|
+
if (code > 127) {
|
|
18209
|
+
return yield* new GitFailed({ message: `git merge-file failed: ${stderr.trim()}` });
|
|
18210
|
+
}
|
|
18211
|
+
return { conflicted: code > 0, text: text.trimEnd() };
|
|
18212
|
+
})).pipe(catchTag2("PlatformError", (error) => fail6(new GitFailed({ message: `git merge-file failed: ${error.message}` })))),
|
|
18213
|
+
read: (args) => scoped2(gen2(function* () {
|
|
18214
|
+
const handle = yield* spawner.spawn(make14("git", args, { cwd, env, extendEnv: true }));
|
|
18215
|
+
const [stdout, stderr, code] = yield* all2([
|
|
18216
|
+
mkString(decodeText(handle.stdout)),
|
|
18217
|
+
mkString(decodeText(handle.stderr)),
|
|
18218
|
+
handle.exitCode
|
|
18219
|
+
], { concurrency: "unbounded" });
|
|
18220
|
+
return code === 0 ? stdout : yield* new GitFailed({
|
|
18221
|
+
message: `git ${args.join(" ")} failed${stderr.trim() === "" ? "" : `: ${stderr.trim()}`}`
|
|
18222
|
+
});
|
|
18223
|
+
})).pipe(catchTag2("PlatformError", (error) => fail6(new GitFailed({ message: `git ${args.join(" ")} failed: ${error.message}` }))))
|
|
18224
|
+
};
|
|
18225
|
+
}));
|
|
18226
|
+
var GitLive = gitLayer();
|
|
18283
18227
|
|
|
18284
18228
|
// src/clients/jj.ts
|
|
18285
18229
|
class JjFailed extends TaggedError2("JjFailed") {
|
|
@@ -18308,27 +18252,6 @@ var jjLayer = ({ cwd, env, quiet = false } = {}) => effect(Jj, gen2(function* ()
|
|
|
18308
18252
|
}));
|
|
18309
18253
|
var JjLive = jjLayer();
|
|
18310
18254
|
|
|
18311
|
-
// src/clients/github/repository.ts
|
|
18312
|
-
var repositoryFromUrl = (url) => {
|
|
18313
|
-
const match = /github\.com[:/](?<owner>[^/]+)\/(?<name>[^/]+?)(?:\.git)?\/?$/u.exec(url.trim());
|
|
18314
|
-
const owner = match?.groups?.["owner"];
|
|
18315
|
-
const name = match?.groups?.["name"];
|
|
18316
|
-
return owner === undefined || name === undefined ? none2() : some2({ name, owner });
|
|
18317
|
-
};
|
|
18318
|
-
|
|
18319
|
-
// src/clients/github/origin.ts
|
|
18320
|
-
var GitHubFromOrigin = unwrap(gen2(function* () {
|
|
18321
|
-
const jj = yield* Jj;
|
|
18322
|
-
const remotes = yield* jj.read(["git", "remote", "list"]);
|
|
18323
|
-
const origin = remotes.split(`
|
|
18324
|
-
`).find((line) => line.startsWith("origin "));
|
|
18325
|
-
const repository = repositoryFromUrl(origin?.slice("origin ".length) ?? "");
|
|
18326
|
-
if (isNone2(repository)) {
|
|
18327
|
-
return yield* new GitHubFailed({ message: "the `origin` remote isn't a GitHub repository" });
|
|
18328
|
-
}
|
|
18329
|
-
return GitHubLive(repository.value);
|
|
18330
|
-
}));
|
|
18331
|
-
|
|
18332
18255
|
// src/cli/gh/stack/changes.ts
|
|
18333
18256
|
class NotAStack extends TaggedError2("NotAStack") {
|
|
18334
18257
|
}
|
|
@@ -18442,6 +18365,221 @@ var chain = (revset, revision) => gen2(function* () {
|
|
|
18442
18365
|
var stackChanges = (revision) => chain(stackRevset(revision), revision);
|
|
18443
18366
|
var downstackChanges = (revision) => chain(downstackRevset(revision), revision);
|
|
18444
18367
|
|
|
18368
|
+
// src/cli/gh/stack/trunk.ts
|
|
18369
|
+
class TrunkMismatch extends TaggedError2("TrunkMismatch") {
|
|
18370
|
+
}
|
|
18371
|
+
var trunkRevset = (branch) => `${JSON.stringify(branch)}@origin`;
|
|
18372
|
+
var checkTrunk = (branch) => gen2(function* () {
|
|
18373
|
+
if ((yield* commitsAt(`trunk() & present(${trunkRevset(branch)})`)).length === 0) {
|
|
18374
|
+
yield* new TrunkMismatch({
|
|
18375
|
+
message: `trunk() isn't ${branch}@origin, GitHub's default branch; run \`jj gh init\``
|
|
18376
|
+
});
|
|
18377
|
+
}
|
|
18378
|
+
});
|
|
18379
|
+
|
|
18380
|
+
// src/cli/gh/init.ts
|
|
18381
|
+
var colocate = gen2(function* () {
|
|
18382
|
+
const fs = yield* FileSystem;
|
|
18383
|
+
const git = yield* Git;
|
|
18384
|
+
const jj = yield* Jj;
|
|
18385
|
+
const resolve = (output) => fs.realPath(output.trim()).pipe(orElseSucceed2(() => ""));
|
|
18386
|
+
const root = yield* resolve(yield* git.read(["rev-parse", "--show-toplevel"]));
|
|
18387
|
+
const jjRoot = yield* jj.read(["root"]).pipe(flatMap4(resolve), orElseSucceed2(() => ""));
|
|
18388
|
+
if (jjRoot !== root) {
|
|
18389
|
+
yield* jj.run(["git", "init", "--colocate", root]);
|
|
18390
|
+
}
|
|
18391
|
+
});
|
|
18392
|
+
var copyIdentity = gen2(function* () {
|
|
18393
|
+
const git = yield* Git;
|
|
18394
|
+
const jj = yield* Jj;
|
|
18395
|
+
for (const key of ["user.name", "user.email"]) {
|
|
18396
|
+
const own = (yield* jj.read(["config", "get", key])).trim();
|
|
18397
|
+
const value = (yield* git.read(["config", key]).pipe(orElseSucceed2(() => ""))).trim();
|
|
18398
|
+
if (own === "" && value !== "") {
|
|
18399
|
+
yield* jj.run(["config", "set", "--repo", key, value]);
|
|
18400
|
+
}
|
|
18401
|
+
}
|
|
18402
|
+
});
|
|
18403
|
+
var defaultBranch = gen2(function* () {
|
|
18404
|
+
const git = yield* Git;
|
|
18405
|
+
const output = yield* git.read(["ls-remote", "--symref", "origin", "HEAD"]);
|
|
18406
|
+
const branch = /^ref: refs\/heads\/(?<branch>\S+)\tHEAD$/mu.exec(output)?.groups?.["branch"];
|
|
18407
|
+
if (branch === undefined) {
|
|
18408
|
+
return yield* new GitFailed({ message: "origin names no default branch" });
|
|
18409
|
+
}
|
|
18410
|
+
return branch;
|
|
18411
|
+
});
|
|
18412
|
+
var setTrunk = gen2(function* () {
|
|
18413
|
+
const jj = yield* Jj;
|
|
18414
|
+
const branch = yield* defaultBranch;
|
|
18415
|
+
if ((yield* commitsAt(`present(${trunkRevset(branch)})`)).length === 0) {
|
|
18416
|
+
yield* jj.run(["git", "fetch", "--remote", "origin", "--branch", `exact:${branch}`]);
|
|
18417
|
+
}
|
|
18418
|
+
if ((yield* bookmarkTargets).get(branch)?.tracked !== true) {
|
|
18419
|
+
yield* jj.run(["bookmark", "track", `exact:${branch}`, "--remote", "origin"]);
|
|
18420
|
+
}
|
|
18421
|
+
yield* jj.run([
|
|
18422
|
+
"config",
|
|
18423
|
+
"set",
|
|
18424
|
+
"--repo",
|
|
18425
|
+
'revset-aliases."trunk()"',
|
|
18426
|
+
JSON.stringify(trunkRevset(branch))
|
|
18427
|
+
]);
|
|
18428
|
+
return branch;
|
|
18429
|
+
});
|
|
18430
|
+
var init = gen2(function* () {
|
|
18431
|
+
const jj = yield* Jj;
|
|
18432
|
+
yield* colocate;
|
|
18433
|
+
yield* copyIdentity;
|
|
18434
|
+
const anonymous = 'mutable() & (author_name(exact:"") | author_email(exact:""))';
|
|
18435
|
+
if ((yield* commitsAt(anonymous)).length > 0) {
|
|
18436
|
+
yield* jj.run(["metaedit", "--quiet", "--update-author", "-r", anonymous]);
|
|
18437
|
+
}
|
|
18438
|
+
const branch = yield* setTrunk;
|
|
18439
|
+
yield* jj.run([
|
|
18440
|
+
"config",
|
|
18441
|
+
"set",
|
|
18442
|
+
"--repo",
|
|
18443
|
+
"aliases.gh",
|
|
18444
|
+
JSON.stringify(["util", "exec", "--", "jj-octo"])
|
|
18445
|
+
]);
|
|
18446
|
+
yield* error(`set up jj, with trunk() as ${branch}@origin`);
|
|
18447
|
+
});
|
|
18448
|
+
var initCommand = make30("init", {}, () => asUserError(init)).pipe(withHelp("Set up a git checkout for the other commands", "Makes it a jj repository colocated with git, unless it already is one, and copies git's name and email into the repository's jj config where jj has none.", "Reads the default branch from `origin`, fetches and tracks it, and sets `trunk()` to it in the repository's config. jj's own `trunk()` guesses from branch names, so without this a default branch it doesn't guess would be the base of pull requests but not of local rebases. `submit`, `merge` and `sync` check that the two agree.", "Sets the `jj gh` alias to run `jj-octo` from `PATH`, in the repository's config. Set it again afterwards to run another build.", "Needs no GitHub token. Running it again changes nothing that is already set up. Run it again after the default branch changes on GitHub."));
|
|
18449
|
+
|
|
18450
|
+
// src/clients/github/openapi.ts
|
|
18451
|
+
var responseOf = () => (schema) => schema;
|
|
18452
|
+
var bodyOf = (body) => bodyJsonUnsafe(body);
|
|
18453
|
+
|
|
18454
|
+
// src/clients/github/types.ts
|
|
18455
|
+
var PullRequestNumberSchema = Number6.pipe(brand2("PullRequestNumber"));
|
|
18456
|
+
var StackNumberSchema = Number6.pipe(brand2("StackNumber"));
|
|
18457
|
+
|
|
18458
|
+
// src/clients/github/client.ts
|
|
18459
|
+
class GitHubFailed extends TaggedError2("GitHubFailed") {
|
|
18460
|
+
}
|
|
18461
|
+
|
|
18462
|
+
class GitHub extends Service()("jj-octo/GitHub") {
|
|
18463
|
+
}
|
|
18464
|
+
var CreatedPullRequest = responseOf()(Struct({ number: PullRequestNumberSchema }));
|
|
18465
|
+
var CreatedStack = responseOf()(Struct({ number: StackNumberSchema }));
|
|
18466
|
+
var PullRequests = responseOf()(ArraySchema(Struct({
|
|
18467
|
+
base: Struct({ ref: String4 }),
|
|
18468
|
+
body: NullOr(String4),
|
|
18469
|
+
draft: optionalKey2(Boolean2),
|
|
18470
|
+
number: PullRequestNumberSchema,
|
|
18471
|
+
title: String4
|
|
18472
|
+
})));
|
|
18473
|
+
var StackedPullRequest = Struct({
|
|
18474
|
+
number: PullRequestNumberSchema,
|
|
18475
|
+
state: Literals(["closed", "open"])
|
|
18476
|
+
});
|
|
18477
|
+
var Stacks = responseOf()(ArraySchema(Struct({ number: StackNumberSchema, pull_requests: ArraySchema(StackedPullRequest) })));
|
|
18478
|
+
var RepositoryDetails = responseOf()(Struct({ default_branch: String4 }));
|
|
18479
|
+
var MergeRequest = Struct({
|
|
18480
|
+
details: Struct({
|
|
18481
|
+
message: String4,
|
|
18482
|
+
uuid: optionalKey2(String4)
|
|
18483
|
+
}),
|
|
18484
|
+
status: Literals(["enqueued", "failed", "merged", "pending"])
|
|
18485
|
+
});
|
|
18486
|
+
var StartedMerge = responseOf()(MergeRequest);
|
|
18487
|
+
var MergeResult = responseOf()(MergeRequest);
|
|
18488
|
+
var token = gen2(function* () {
|
|
18489
|
+
const fromEnvironment = yield* option2(Redacted2("GH_TOKEN").pipe(orElse(() => Redacted2("GITHUB_TOKEN"))));
|
|
18490
|
+
if (isSome2(fromEnvironment)) {
|
|
18491
|
+
return fromEnvironment.value;
|
|
18492
|
+
}
|
|
18493
|
+
const spawner = yield* ChildProcessSpawner;
|
|
18494
|
+
const stored = (yield* spawner.string(make14("gh", ["auth", "token"]))).trim();
|
|
18495
|
+
return stored === "" ? yield* fail6("no token") : make19(stored);
|
|
18496
|
+
}).pipe(mapError2(() => new GitHubFailed({ message: "no GitHub token: set GH_TOKEN, or run `gh auth login`" })));
|
|
18497
|
+
var GitHubLive = (repository) => {
|
|
18498
|
+
const repo = (path) => `/repos/${repository.owner}/${repository.name}${path}`;
|
|
18499
|
+
return effect(GitHub, gen2(function* () {
|
|
18500
|
+
const bearer = yield* token;
|
|
18501
|
+
const client = (yield* HttpClient).pipe(mapRequest((request) => request.pipe(prependUrl("https://api.github.com"), acceptJson, bearerToken(bearer), setHeaders({
|
|
18502
|
+
"user-agent": "jj-octo",
|
|
18503
|
+
"x-github-api-version": "2022-11-28"
|
|
18504
|
+
}))));
|
|
18505
|
+
const send = (request) => client.execute(request).pipe(flatMap4((response) => response.status < 400 ? succeed6(response) : response.text.pipe(orElseSucceed2(() => ""), flatMap4((text) => fail6(new GitHubFailed({
|
|
18506
|
+
message: `${request.method} ${request.url} returned ${response.status}: ${text}`
|
|
18507
|
+
}))))), catchTag2("HttpClientError", (error) => fail6(new GitHubFailed({ message: error.message }))));
|
|
18508
|
+
const json = (schema) => (request) => send(request).pipe(flatMap4(schemaBodyJson(schema)), catchTags2({
|
|
18509
|
+
HttpClientError: (error) => fail6(new GitHubFailed({ message: error.message })),
|
|
18510
|
+
SchemaError: (error) => fail6(new GitHubFailed({
|
|
18511
|
+
message: `unexpected response to ${request.url}: ${error.message}`
|
|
18512
|
+
}))
|
|
18513
|
+
}));
|
|
18514
|
+
const stackOf = (pullRequest) => json(Stacks)(get4(repo(`/stacks?pull_request=${pullRequest}`))).pipe(map7((stacks) => map(fromNullishOr(stacks[0]), (stack) => ({
|
|
18515
|
+
number: stack.number,
|
|
18516
|
+
open: stack.pull_requests.filter(({ state }) => state === "open").map(({ number }) => number)
|
|
18517
|
+
}))));
|
|
18518
|
+
const mergeRequest = (path) => json(MergeResult)(get4(repo(path)));
|
|
18519
|
+
return {
|
|
18520
|
+
addToStack: (stack, pullRequests) => send(post(repo(`/stacks/${stack}/add`)).pipe(bodyOf({ pull_requests: [...pullRequests] }))).pipe(asVoid2),
|
|
18521
|
+
createPullRequest: ({ base, body, draft, head, title }) => json(CreatedPullRequest)(post(repo("/pulls")).pipe(bodyOf({ base, body, draft, head, title }))).pipe(map7(({ number }) => number)),
|
|
18522
|
+
createStack: (pullRequests) => json(CreatedStack)(post(repo("/stacks")).pipe(bodyOf({ pull_requests: [...pullRequests] }))).pipe(map7(({ number }) => number)),
|
|
18523
|
+
defaultBranch: json(RepositoryDetails)(get4(repo(""))).pipe(map7(({ default_branch }) => default_branch)),
|
|
18524
|
+
mergePullRequest: (pullRequest) => send(put(repo(`/pulls/${pullRequest}/merge`)).pipe(bodyOf({ merge_method: "squash" }))).pipe(asVoid2),
|
|
18525
|
+
mergeStack: (pullRequest) => gen2(function* () {
|
|
18526
|
+
const started = yield* json(StartedMerge)(put(repo(`/pulls/${pullRequest}/merge-async`)).pipe(bodyOf({
|
|
18527
|
+
merge_action: "default",
|
|
18528
|
+
merge_method: "squash"
|
|
18529
|
+
})));
|
|
18530
|
+
const { uuid } = started.details;
|
|
18531
|
+
if (uuid === undefined) {
|
|
18532
|
+
return yield* new GitHubFailed({
|
|
18533
|
+
message: `merging #${pullRequest} returned no request to follow`
|
|
18534
|
+
});
|
|
18535
|
+
}
|
|
18536
|
+
const settled = yield* mergeRequest(`/pulls/${pullRequest}/merge-async/${uuid}`).pipe(repeat2({
|
|
18537
|
+
schedule: spaced("5 seconds"),
|
|
18538
|
+
until: ({ status }) => status !== "pending"
|
|
18539
|
+
}));
|
|
18540
|
+
if (settled.status === "merged" || settled.status === "enqueued") {
|
|
18541
|
+
return settled.status;
|
|
18542
|
+
}
|
|
18543
|
+
return yield* new GitHubFailed({
|
|
18544
|
+
message: `merging #${pullRequest} ended ${settled.status}: ${settled.details.message}`
|
|
18545
|
+
});
|
|
18546
|
+
}),
|
|
18547
|
+
openPullRequest: (branch) => json(PullRequests)(get4(repo(`/pulls?state=open&head=${repository.owner}:${encodeURIComponent(branch)}`))).pipe(map7((found) => map(fromNullishOr(found[0]), ({ base, body, draft, number, title }) => ({
|
|
18548
|
+
base: base.ref,
|
|
18549
|
+
body: body ?? "",
|
|
18550
|
+
draft: draft ?? false,
|
|
18551
|
+
number,
|
|
18552
|
+
title
|
|
18553
|
+
})))),
|
|
18554
|
+
setBase: (pullRequest, base) => send(patch(repo(`/pulls/${pullRequest}`)).pipe(bodyOf({ base }))).pipe(asVoid2),
|
|
18555
|
+
setDescription: (pullRequest, { body, title }) => send(patch(repo(`/pulls/${pullRequest}`)).pipe(bodyOf({ body, title }))).pipe(asVoid2),
|
|
18556
|
+
stackOf,
|
|
18557
|
+
unstack: (stack) => send(post(repo(`/stacks/${stack}/unstack`))).pipe(asVoid2)
|
|
18558
|
+
};
|
|
18559
|
+
}));
|
|
18560
|
+
};
|
|
18561
|
+
|
|
18562
|
+
// src/clients/github/repository.ts
|
|
18563
|
+
var repositoryFromUrl = (url) => {
|
|
18564
|
+
const match = /github\.com[:/](?<owner>[^/]+)\/(?<name>[^/]+?)(?:\.git)?\/?$/u.exec(url.trim());
|
|
18565
|
+
const owner = match?.groups?.["owner"];
|
|
18566
|
+
const name = match?.groups?.["name"];
|
|
18567
|
+
return owner === undefined || name === undefined ? none2() : some2({ name, owner });
|
|
18568
|
+
};
|
|
18569
|
+
|
|
18570
|
+
// src/clients/github/origin.ts
|
|
18571
|
+
var GitHubFromOrigin = unwrap(gen2(function* () {
|
|
18572
|
+
const jj = yield* Jj;
|
|
18573
|
+
const remotes = yield* jj.read(["git", "remote", "list"]);
|
|
18574
|
+
const origin = remotes.split(`
|
|
18575
|
+
`).find((line) => line.startsWith("origin "));
|
|
18576
|
+
const repository = repositoryFromUrl(origin?.slice("origin ".length) ?? "");
|
|
18577
|
+
if (isNone2(repository)) {
|
|
18578
|
+
return yield* new GitHubFailed({ message: "the `origin` remote isn't a GitHub repository" });
|
|
18579
|
+
}
|
|
18580
|
+
return GitHubLive(repository.value);
|
|
18581
|
+
}));
|
|
18582
|
+
|
|
18445
18583
|
// src/cli/gh/log.ts
|
|
18446
18584
|
var logLines2 = (entries) => {
|
|
18447
18585
|
const rows = entries.toReversed().map(({ bookmark, pullRequest, pushed }) => [
|
|
@@ -18469,50 +18607,6 @@ var log2 = (revision) => gen2(function* () {
|
|
|
18469
18607
|
var logCommand = make30("log", { revision: revisionFlag("A change of the stack to show") }, ({ revision }) => asUserError(log2(revision).pipe(provide3(GitHubFromOrigin))).pipe(flatMap4((printed) => log(printed.join(`
|
|
18470
18608
|
`))))).pipe(withHelp("Show changes and their pull requests", "Prints one line per change, top first: its bookmark, its pull request with whether that is a draft or ready for review, and whether the change is pushed, changed since the push, or not pushed.", "Fails if the changes aren't a chain, if one has conflicts, or if one doesn't have exactly one bookmark. `submit` makes the same check before it changes anything.", "Changes nothing, locally or on GitHub."));
|
|
18471
18609
|
|
|
18472
|
-
// src/clients/git.ts
|
|
18473
|
-
class GitFailed extends TaggedError2("GitFailed") {
|
|
18474
|
-
}
|
|
18475
|
-
|
|
18476
|
-
class Git extends Service()("jj-octo/Git") {
|
|
18477
|
-
}
|
|
18478
|
-
var GitLive = effect(Git, gen2(function* () {
|
|
18479
|
-
const spawner = yield* ChildProcessSpawner;
|
|
18480
|
-
const fs = yield* FileSystem;
|
|
18481
|
-
const path = yield* Path;
|
|
18482
|
-
return {
|
|
18483
|
-
mergeText: ({ base, local, remote }) => scoped2(gen2(function* () {
|
|
18484
|
-
const directory = yield* fs.makeTempDirectoryScoped();
|
|
18485
|
-
const written = { base: `${base}
|
|
18486
|
-
`, github: `${remote}
|
|
18487
|
-
`, local: `${local}
|
|
18488
|
-
` };
|
|
18489
|
-
yield* all2(Object.entries(written).map(([name, text]) => fs.writeFileString(path.join(directory, name), text)));
|
|
18490
|
-
const handle = yield* spawner.spawn(make14("git", [
|
|
18491
|
-
"merge-file",
|
|
18492
|
-
"-p",
|
|
18493
|
-
"-L",
|
|
18494
|
-
"local",
|
|
18495
|
-
"-L",
|
|
18496
|
-
"base",
|
|
18497
|
-
"-L",
|
|
18498
|
-
"GitHub",
|
|
18499
|
-
"local",
|
|
18500
|
-
"base",
|
|
18501
|
-
"github"
|
|
18502
|
-
], { cwd: directory }));
|
|
18503
|
-
const [text, stderr, code] = yield* all2([
|
|
18504
|
-
mkString(decodeText(handle.stdout)),
|
|
18505
|
-
mkString(decodeText(handle.stderr)),
|
|
18506
|
-
handle.exitCode
|
|
18507
|
-
], { concurrency: "unbounded" });
|
|
18508
|
-
if (code > 127) {
|
|
18509
|
-
return yield* new GitFailed({ message: `git merge-file failed: ${stderr.trim()}` });
|
|
18510
|
-
}
|
|
18511
|
-
return { conflicted: code > 0, text: text.trimEnd() };
|
|
18512
|
-
})).pipe(catchTag2("PlatformError", (error) => fail6(new GitFailed({ message: `git merge-file failed: ${error.message}` }))))
|
|
18513
|
-
};
|
|
18514
|
-
}));
|
|
18515
|
-
|
|
18516
18610
|
// src/cli/gh/stack/plan.ts
|
|
18517
18611
|
var basesFor = (branches, trunk) => [
|
|
18518
18612
|
trunk,
|
|
@@ -18687,7 +18781,10 @@ var publish = (stack, { draft, overwriteDescriptions }) => gen2(function* () {
|
|
|
18687
18781
|
}
|
|
18688
18782
|
yield* register(dissolve ? none2() : existing, numbers);
|
|
18689
18783
|
});
|
|
18690
|
-
var submit = (revision, options) =>
|
|
18784
|
+
var submit = (revision, options) => gen2(function* () {
|
|
18785
|
+
yield* checkTrunk(yield* (yield* GitHub).defaultBranch);
|
|
18786
|
+
yield* publish(yield* downstackChanges(revision), options);
|
|
18787
|
+
});
|
|
18691
18788
|
var republish = (revision) => gen2(function* () {
|
|
18692
18789
|
if ((yield* changesIn(stackRevset(revision))).length === 0) {
|
|
18693
18790
|
return;
|
|
@@ -18820,7 +18917,10 @@ var sync4 = (required = []) => gen2(function* () {
|
|
|
18820
18917
|
});
|
|
18821
18918
|
}
|
|
18822
18919
|
});
|
|
18823
|
-
var syncCommand = make30("sync", {}, () => asUserError(
|
|
18920
|
+
var syncCommand = make30("sync", {}, () => asUserError(gen2(function* () {
|
|
18921
|
+
yield* checkTrunk(yield* (yield* GitHub).defaultBranch);
|
|
18922
|
+
yield* sync4();
|
|
18923
|
+
}).pipe(provide3(GitHubFromOrigin)))).pipe(withHelp("Fetch, and rebase every change onto the trunk", "Fetches, then rebases every change that isn't merged yet onto the trunk, keeping each on the changes below it. A change that has merged is empty after the rebase, so it's abandoned and its bookmark forgotten.", "A stack that would conflict with the trunk is left where it was, and named. Rebase it yourself with `jj rebase` when you want to resolve it.", "Commits added to a branch on GitHub are squashed into the branch's local change, whether or not that change was also edited locally. Where GitHub only rebased a branch, the local change is kept.", "Fails before fetching if `trunk()` isn't GitHub's default branch, which `jj gh init` sets.", "Pushes nothing. Run `jj gh submit` to update the pull requests of a change and the ones below it."));
|
|
18824
18924
|
|
|
18825
18925
|
// src/cli/gh/merge.ts
|
|
18826
18926
|
class NotMergeable extends TaggedError2("NotMergeable") {
|
|
@@ -18836,7 +18936,7 @@ var openPullRequestOf = (bookmark) => gen2(function* () {
|
|
|
18836
18936
|
}
|
|
18837
18937
|
return opened.value;
|
|
18838
18938
|
});
|
|
18839
|
-
var mergeOnGitHub = ({ base, number }) => gen2(function* () {
|
|
18939
|
+
var mergeOnGitHub = ({ base, number }, trunk) => gen2(function* () {
|
|
18840
18940
|
const github = yield* GitHub;
|
|
18841
18941
|
const stack = yield* github.stackOf(number);
|
|
18842
18942
|
if (isSome2(stack)) {
|
|
@@ -18844,7 +18944,7 @@ var mergeOnGitHub = ({ base, number }) => gen2(function* () {
|
|
|
18844
18944
|
yield* error(`merging ${numbered(open.slice(0, open.indexOf(number) + 1))}`);
|
|
18845
18945
|
return yield* github.mergeStack(number);
|
|
18846
18946
|
}
|
|
18847
|
-
if (base !==
|
|
18947
|
+
if (base !== trunk) {
|
|
18848
18948
|
return yield* new NotMergeable({
|
|
18849
18949
|
message: `#${number} isn't in a GitHub stack, and its base is ${base}, so merging it wouldn't reach the trunk; run \`jj gh submit\` to register the stack, or merge the changes below it first`
|
|
18850
18950
|
});
|
|
@@ -18854,6 +18954,8 @@ var mergeOnGitHub = ({ base, number }) => gen2(function* () {
|
|
|
18854
18954
|
return "merged";
|
|
18855
18955
|
});
|
|
18856
18956
|
var merge5 = (revision) => gen2(function* () {
|
|
18957
|
+
const trunk = yield* (yield* GitHub).defaultBranch;
|
|
18958
|
+
yield* checkTrunk(trunk);
|
|
18857
18959
|
const changes = yield* downstackChanges(revision);
|
|
18858
18960
|
const top = changes.at(-1)?.bookmark ?? "";
|
|
18859
18961
|
const targets = yield* bookmarkTargets;
|
|
@@ -18866,7 +18968,7 @@ var merge5 = (revision) => gen2(function* () {
|
|
|
18866
18968
|
const pullRequest = yield* openPullRequestOf(top);
|
|
18867
18969
|
const above = yield* bookmarksAt(upstackRevset(bookmarkRevset(top)));
|
|
18868
18970
|
const stackChanges = yield* changeIdsAt(stackRevset(revision));
|
|
18869
|
-
if ((yield* mergeOnGitHub(pullRequest)) === "enqueued") {
|
|
18971
|
+
if ((yield* mergeOnGitHub(pullRequest, trunk)) === "enqueued") {
|
|
18870
18972
|
yield* error("queued to merge; run `jj gh sync` once it has");
|
|
18871
18973
|
} else {
|
|
18872
18974
|
yield* error(`merged up to #${pullRequest.number}`);
|
|
@@ -18889,7 +18991,7 @@ var submitCommand = make30("submit", {
|
|
|
18889
18991
|
}, ({ overwriteDescriptions, publish, revision }) => asUserError(submit(revision, { draft: !publish, overwriteDescriptions }).pipe(provide3(GitHubFromOrigin)))).pipe(withHelp("Push a change and the ones below it, and open their pull requests", "Pushes the bookmark of each of those changes. An empty working copy means the change below it, so from the top of a stack this submits all of it. Opens a pull request for each branch that has none, with the title and body from the change's description. Points each pull request at the branch below it, and the bottom one at the default branch.", "New pull requests are opened as drafts, or ready for review with `--publish`. The flag doesn't change a pull request that already exists.", "Keeps each pull request's title and body the same as its change's description. A description edited on GitHub is written into the change, one edited locally is sent to the pull request, and edits on both sides are merged line by line. Edits to the same lines stop the submit before anything is pushed.", "Registers the pull requests as one GitHub stack. A stack of one change is a single pull request, which GitHub has no stack for.", "If changes were reordered, inserted or dropped since the last submit, the GitHub stack is dissolved and registered again under a new number. The pull requests stay open, including the one of a dropped change."));
|
|
18890
18992
|
|
|
18891
18993
|
// src/cli/gh/command.ts
|
|
18892
|
-
var jjGhCommand = make30("jj gh").pipe(withSubcommands([logCommand, mergeCommand, submitCommand, syncCommand]), withHelp("Commands for publishing jj changes as GitHub pull requests", "A stack is a chain of changes, each published as one pull request on the branch its bookmark names. Each pull request targets the branch of the change below it, so it shows only that change's diff. A single change is a stack of one, and is published as an ordinary pull request.", "The stack of a change is every change connected to it through mutable changes. An empty working copy with no description isn't part of it. `log` shows the whole stack. `submit` and `merge` act on a change and the ones below it, which must be a chain: no merges, no conflicts, and exactly one bookmark on each change.", "The repository is the one the `origin` remote points to. The token is read from `GH_TOKEN` or `GITHUB_TOKEN`, and otherwise from `gh auth token`."));
|
|
18994
|
+
var jjGhCommand = make30("jj gh").pipe(withSubcommands([initCommand, logCommand, mergeCommand, submitCommand, syncCommand]), withHelp("Commands for publishing jj changes as GitHub pull requests", "A stack is a chain of changes, each published as one pull request on the branch its bookmark names. Each pull request targets the branch of the change below it, so it shows only that change's diff. A single change is a stack of one, and is published as an ordinary pull request.", "The stack of a change is every change connected to it through mutable changes. An empty working copy with no description isn't part of it. `log` shows the whole stack. `submit` and `merge` act on a change and the ones below it, which must be a chain: no merges, no conflicts, and exactly one bookmark on each change.", "The repository is the one the `origin` remote points to. The token is read from `GH_TOKEN` or `GITHUB_TOKEN`, and otherwise from `gh auth token`."));
|
|
18893
18995
|
|
|
18894
18996
|
// src/main.ts
|
|
18895
18997
|
runMain2(run5(jjGhCommand, { version: package_default.version }).pipe(provide3([GitLive, JjLive]), provide3(layer13({
|