jj-octo 0.5.0 → 0.7.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 +2 -12
- package/dist/jj-octo.js +496 -254
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,6 +28,8 @@ mise use --global github:kkom/jj-octo@X.Y.Z
|
|
|
28
28
|
|
|
29
29
|
Then run `jj-octo init` once in each checkout. It makes the checkout a jj repository colocated with git, makes GitHub's default branch `trunk()`, and sets the `jj gh` alias.
|
|
30
30
|
|
|
31
|
+
Run `jj-octo init` again after upgrading jj-octo, to set up what the new version needs. `jj-octo doctor` reports what isn't set up, and changes nothing.
|
|
32
|
+
|
|
31
33
|
## GitHub access
|
|
32
34
|
|
|
33
35
|
`jj-octo` reads the repository from the `origin` remote.
|
|
@@ -50,18 +52,6 @@ jj config set --repo jj-octo.ignore-claude-code-footer true
|
|
|
50
52
|
|
|
51
53
|
There, the [GitHub proxy](https://code.claude.com/docs/en/cloud-environments#github-proxy) carries all GitHub traffic. It serves GitHub API requests only for the repositories attached to the session, and rejects GraphQL requests. That's why `jj-octo` uses only GitHub's REST API, while `gh pr` and other tools that depend on GraphQL fail there.
|
|
52
54
|
|
|
53
|
-
mise reads this repository's releases through the GitHub API, so it can't install them unless the session has this repository attached. Install the npm package instead:
|
|
54
|
-
|
|
55
|
-
```toml
|
|
56
|
-
[tools]
|
|
57
|
-
bun = "A.B.C"
|
|
58
|
-
|
|
59
|
-
[tools."npm:jj-octo"]
|
|
60
|
-
version = "X.Y.Z"
|
|
61
|
-
# mise holds back a package that is new or rarely downloaded, and this approves it.
|
|
62
|
-
allow_low_downloads = true
|
|
63
|
-
```
|
|
64
|
-
|
|
65
55
|
The session sets `HTTPS_PROXY`, `NODE_EXTRA_CA_CERTS`, and `GH_TOKEN` to the placeholder `proxy-injected`. The GitHub proxy replaces the placeholder with your real credentials. So `jj-octo` works there with its default settings.
|
|
66
56
|
|
|
67
57
|
### In a self-hosted environment
|
package/dist/jj-octo.js
CHANGED
|
@@ -18239,7 +18239,7 @@ var layer14 = /* @__PURE__ */ layerMergedContext(/* @__PURE__ */ succeed6(fetch)
|
|
|
18239
18239
|
// package.json
|
|
18240
18240
|
var package_default = {
|
|
18241
18241
|
name: "jj-octo",
|
|
18242
|
-
version: "0.
|
|
18242
|
+
version: "0.7.0",
|
|
18243
18243
|
description: "A jj subcommand for GitHub that supports stacked pull requests and works in Claude Code cloud sessions",
|
|
18244
18244
|
license: "MIT",
|
|
18245
18245
|
repository: {
|
|
@@ -18343,12 +18343,30 @@ var gitLayer = ({ cwd, env } = {}) => effect(Git, gen2(function* () {
|
|
|
18343
18343
|
handle.exitCode
|
|
18344
18344
|
], { concurrency: "unbounded" });
|
|
18345
18345
|
return code === 0 ? stdout : yield* new GitFailed({
|
|
18346
|
+
exitCode: code,
|
|
18346
18347
|
message: `git ${args.join(" ")} failed${stderr.trim() === "" ? "" : `: ${stderr.trim()}`}`
|
|
18347
18348
|
});
|
|
18348
18349
|
})).pipe(catchTag2("PlatformError", (error) => fail6(new GitFailed({ message: `git ${args.join(" ")} failed: ${error.message}` }))))
|
|
18349
18350
|
};
|
|
18350
18351
|
}));
|
|
18351
18352
|
var GitLive = gitLayer();
|
|
18353
|
+
var gitConfig = (args) => gen2(function* () {
|
|
18354
|
+
const git = yield* Git;
|
|
18355
|
+
const value = yield* git.read(["config", ...args]).pipe(catch_2((error) => error.exitCode === 1 ? succeed6("") : fail6(error)));
|
|
18356
|
+
return value.trim();
|
|
18357
|
+
});
|
|
18358
|
+
var gitDirectories = gen2(function* () {
|
|
18359
|
+
const git = yield* Git;
|
|
18360
|
+
const [topLevel = "", gitDirectory = "", commonDirectory = ""] = (yield* git.read([
|
|
18361
|
+
"rev-parse",
|
|
18362
|
+
"--path-format=absolute",
|
|
18363
|
+
"--show-toplevel",
|
|
18364
|
+
"--git-dir",
|
|
18365
|
+
"--git-common-dir"
|
|
18366
|
+
])).split(`
|
|
18367
|
+
`);
|
|
18368
|
+
return { commonDirectory, gitDirectory, topLevel };
|
|
18369
|
+
});
|
|
18352
18370
|
|
|
18353
18371
|
// src/clients/jj.ts
|
|
18354
18372
|
class JjFailed extends TaggedError2("JjFailed") {
|
|
@@ -18376,6 +18394,42 @@ var jjLayer = ({ cwd, env, quiet = false } = {}) => effect(Jj, gen2(function* ()
|
|
|
18376
18394
|
return { read, run };
|
|
18377
18395
|
}));
|
|
18378
18396
|
var JjLive = jjLayer();
|
|
18397
|
+
var ENTRY = String.raw`json(name) ++ " " ++ json(value) ++ "\n"`;
|
|
18398
|
+
var readJjConfig = (key, { repo = false } = {}) => gen2(function* () {
|
|
18399
|
+
const jj = yield* Jj;
|
|
18400
|
+
const scope = repo ? ["--repo"] : [];
|
|
18401
|
+
const output = yield* jj.read(["config", "list", ...scope, key, "-T", ENTRY]);
|
|
18402
|
+
const lines = output.split(`
|
|
18403
|
+
`).filter((line) => line !== "");
|
|
18404
|
+
const prefix = `${JSON.stringify(key)} `;
|
|
18405
|
+
if (lines.some((line) => !line.startsWith(prefix))) {
|
|
18406
|
+
return yield* new JjFailed({ message: `${key} is a table; set it to a single value` });
|
|
18407
|
+
}
|
|
18408
|
+
const [line] = lines;
|
|
18409
|
+
return line === undefined ? undefined : JSON.parse(line.slice(prefix.length));
|
|
18410
|
+
});
|
|
18411
|
+
var jjConfig = (key) => gen2(function* () {
|
|
18412
|
+
const value = yield* readJjConfig(key);
|
|
18413
|
+
if (value === undefined || typeof value === "string") {
|
|
18414
|
+
return value ?? "";
|
|
18415
|
+
}
|
|
18416
|
+
return yield* new JjFailed({
|
|
18417
|
+
message: `${key} is ${JSON.stringify(value)}; set it to a string`
|
|
18418
|
+
});
|
|
18419
|
+
});
|
|
18420
|
+
var jjFlag = (key) => gen2(function* () {
|
|
18421
|
+
const value = yield* readJjConfig(key);
|
|
18422
|
+
if (value === undefined || typeof value === "boolean") {
|
|
18423
|
+
return value === true;
|
|
18424
|
+
}
|
|
18425
|
+
return yield* new JjFailed({
|
|
18426
|
+
message: `${key} is ${JSON.stringify(value)}; set it to true or false`
|
|
18427
|
+
});
|
|
18428
|
+
});
|
|
18429
|
+
var setJjConfig = (key, value) => gen2(function* () {
|
|
18430
|
+
const jj = yield* Jj;
|
|
18431
|
+
yield* jj.run(["config", "set", "--repo", key, JSON.stringify(value)]);
|
|
18432
|
+
});
|
|
18379
18433
|
|
|
18380
18434
|
// src/cli/gh/stack/changes.ts
|
|
18381
18435
|
class NotAStack extends TaggedError2("NotAStack") {
|
|
@@ -18385,6 +18439,7 @@ var SCRATCH = '(working_copies() & empty() & description(exact:""))';
|
|
|
18385
18439
|
var stackRevset = (revision) => `(reachable(${revision}, mutable()) & (${STACK_ROOTS})::) ~ ${SCRATCH}`;
|
|
18386
18440
|
var downstackRevset = (revision) => `::(${revision}) & (${stackRevset(revision)})`;
|
|
18387
18441
|
var upstackRevset = (revision) => `(${revision})+:: & (${stackRevset(revision)})`;
|
|
18442
|
+
var anyOf = (revsets) => ["none()", ...revsets].join(" | ");
|
|
18388
18443
|
var bookmarkRevset = (bookmark) => `bookmarks(exact:"${bookmark}")`;
|
|
18389
18444
|
var lines2 = (output) => output.split(`
|
|
18390
18445
|
`).filter((line) => line !== "");
|
|
@@ -18490,100 +18545,6 @@ var chain = (revset, revision) => gen2(function* () {
|
|
|
18490
18545
|
var stackChanges = (revision) => chain(stackRevset(revision), revision);
|
|
18491
18546
|
var downstackChanges = (revision) => chain(downstackRevset(revision), revision);
|
|
18492
18547
|
|
|
18493
|
-
// src/cli/gh/stack/trunk.ts
|
|
18494
|
-
class TrunkMismatch extends TaggedError2("TrunkMismatch") {
|
|
18495
|
-
}
|
|
18496
|
-
var trunkRevset = (branch) => `${JSON.stringify(branch)}@origin`;
|
|
18497
|
-
var checkTrunk = (branch) => gen2(function* () {
|
|
18498
|
-
if ((yield* commitsAt(`trunk() & present(${trunkRevset(branch)})`)).length === 0) {
|
|
18499
|
-
yield* new TrunkMismatch({
|
|
18500
|
-
message: `trunk() isn't ${branch}@origin, GitHub's default branch; run \`jj gh init\``
|
|
18501
|
-
});
|
|
18502
|
-
}
|
|
18503
|
-
});
|
|
18504
|
-
|
|
18505
|
-
// src/cli/gh/init.ts
|
|
18506
|
-
var colocate = gen2(function* () {
|
|
18507
|
-
const fs = yield* FileSystem;
|
|
18508
|
-
const git = yield* Git;
|
|
18509
|
-
const jj = yield* Jj;
|
|
18510
|
-
const resolve = (output) => fs.realPath(output.trim()).pipe(orElseSucceed2(() => ""));
|
|
18511
|
-
const root = yield* resolve(yield* git.read(["rev-parse", "--show-toplevel"]));
|
|
18512
|
-
const jjRoot = yield* jj.read(["root"]).pipe(flatMap4(resolve), orElseSucceed2(() => ""));
|
|
18513
|
-
if (jjRoot !== root) {
|
|
18514
|
-
const [gitDirectory, commonDirectory] = (yield* git.read([
|
|
18515
|
-
"rev-parse",
|
|
18516
|
-
"--path-format=absolute",
|
|
18517
|
-
"--git-dir",
|
|
18518
|
-
"--git-common-dir"
|
|
18519
|
-
])).split(`
|
|
18520
|
-
`);
|
|
18521
|
-
if (gitDirectory !== commonDirectory) {
|
|
18522
|
-
yield* new GitFailed({
|
|
18523
|
-
message: "this is a git worktree, where jj would make a second repository; run `jj workspace add` in the main checkout instead"
|
|
18524
|
-
});
|
|
18525
|
-
}
|
|
18526
|
-
yield* jj.run(["git", "init", "--colocate", root]);
|
|
18527
|
-
}
|
|
18528
|
-
});
|
|
18529
|
-
var copyIdentity = gen2(function* () {
|
|
18530
|
-
const git = yield* Git;
|
|
18531
|
-
const jj = yield* Jj;
|
|
18532
|
-
for (const key of ["user.name", "user.email"]) {
|
|
18533
|
-
const own = (yield* jj.read(["config", "get", key])).trim();
|
|
18534
|
-
const value = (yield* git.read(["config", key]).pipe(orElseSucceed2(() => ""))).trim();
|
|
18535
|
-
if (own === "" && value !== "") {
|
|
18536
|
-
yield* jj.run(["config", "set", "--repo", key, JSON.stringify(value)]);
|
|
18537
|
-
}
|
|
18538
|
-
}
|
|
18539
|
-
});
|
|
18540
|
-
var defaultBranch = gen2(function* () {
|
|
18541
|
-
const git = yield* Git;
|
|
18542
|
-
const output = yield* git.read(["ls-remote", "--symref", "origin", "HEAD"]);
|
|
18543
|
-
const branch = /^ref: refs\/heads\/(?<branch>\S+)\tHEAD$/mu.exec(output)?.groups?.["branch"];
|
|
18544
|
-
if (branch === undefined) {
|
|
18545
|
-
return yield* new GitFailed({ message: "origin names no default branch" });
|
|
18546
|
-
}
|
|
18547
|
-
return branch;
|
|
18548
|
-
});
|
|
18549
|
-
var setTrunk = gen2(function* () {
|
|
18550
|
-
const jj = yield* Jj;
|
|
18551
|
-
const branch = yield* defaultBranch;
|
|
18552
|
-
if ((yield* commitsAt(`present(${trunkRevset(branch)})`)).length === 0) {
|
|
18553
|
-
yield* jj.run(["git", "fetch", "--remote", "origin", "--branch", `exact:${branch}`]);
|
|
18554
|
-
}
|
|
18555
|
-
if ((yield* bookmarkTargets).get(branch)?.tracked !== true) {
|
|
18556
|
-
yield* jj.run(["bookmark", "track", `exact:${branch}`, "--remote", "origin"]);
|
|
18557
|
-
}
|
|
18558
|
-
yield* jj.run([
|
|
18559
|
-
"config",
|
|
18560
|
-
"set",
|
|
18561
|
-
"--repo",
|
|
18562
|
-
'revset-aliases."trunk()"',
|
|
18563
|
-
JSON.stringify(trunkRevset(branch))
|
|
18564
|
-
]);
|
|
18565
|
-
return branch;
|
|
18566
|
-
});
|
|
18567
|
-
var init = gen2(function* () {
|
|
18568
|
-
const jj = yield* Jj;
|
|
18569
|
-
yield* colocate;
|
|
18570
|
-
yield* copyIdentity;
|
|
18571
|
-
const anonymous = 'mutable() & (author_name(exact:"") | author_email(exact:""))';
|
|
18572
|
-
if ((yield* commitsAt(anonymous)).length > 0) {
|
|
18573
|
-
yield* jj.run(["metaedit", "--quiet", "--update-author", "-r", anonymous]);
|
|
18574
|
-
}
|
|
18575
|
-
const branch = yield* setTrunk;
|
|
18576
|
-
yield* jj.run([
|
|
18577
|
-
"config",
|
|
18578
|
-
"set",
|
|
18579
|
-
"--repo",
|
|
18580
|
-
"aliases.gh",
|
|
18581
|
-
JSON.stringify(["util", "exec", "--", "jj-octo"])
|
|
18582
|
-
]);
|
|
18583
|
-
yield* error(`set up jj, with trunk() as ${branch}@origin`);
|
|
18584
|
-
});
|
|
18585
|
-
var initCommand = make31("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."));
|
|
18586
|
-
|
|
18587
18548
|
// src/clients/github/openapi.ts
|
|
18588
18549
|
var responseOf = () => (schema) => schema;
|
|
18589
18550
|
var bodyOf = (body) => bodyJsonUnsafe(body);
|
|
@@ -18699,6 +18660,155 @@ var GitHubLive = (repository, connection) => {
|
|
|
18699
18660
|
}));
|
|
18700
18661
|
};
|
|
18701
18662
|
|
|
18663
|
+
// src/cli/gh/stack/trunk.ts
|
|
18664
|
+
class TrunkMismatch extends TaggedError2("TrunkMismatch") {
|
|
18665
|
+
}
|
|
18666
|
+
var trunkRevset = (branch) => `${JSON.stringify(branch)}@origin`;
|
|
18667
|
+
var checkTrunk = (branch) => gen2(function* () {
|
|
18668
|
+
if ((yield* commitsAt(`trunk() & present(${trunkRevset(branch)})`)).length === 0) {
|
|
18669
|
+
yield* new TrunkMismatch({
|
|
18670
|
+
message: `trunk() isn't ${branch}@origin, GitHub's default branch; run \`jj-octo init\``
|
|
18671
|
+
});
|
|
18672
|
+
}
|
|
18673
|
+
});
|
|
18674
|
+
var checkedTrunk = gen2(function* () {
|
|
18675
|
+
const branch = yield* (yield* GitHub).defaultBranch;
|
|
18676
|
+
yield* checkTrunk(branch);
|
|
18677
|
+
return branch;
|
|
18678
|
+
});
|
|
18679
|
+
|
|
18680
|
+
// src/cli/gh/setup.ts
|
|
18681
|
+
class NotSetUp extends TaggedError2("NotSetUp") {
|
|
18682
|
+
}
|
|
18683
|
+
var forInit = (message) => ({ byInit: true, message });
|
|
18684
|
+
var advice = ({ byInit, message }) => byInit ? `${message}; run \`jj-octo init\`` : message;
|
|
18685
|
+
var defaultBranch = gen2(function* () {
|
|
18686
|
+
const git = yield* Git;
|
|
18687
|
+
const output = yield* git.read(["ls-remote", "--symref", "origin", "HEAD"]);
|
|
18688
|
+
const branch = /^ref: refs\/heads\/(?<branch>\S+)\tHEAD$/mu.exec(output)?.groups?.["branch"];
|
|
18689
|
+
if (branch === undefined) {
|
|
18690
|
+
return yield* new GitFailed({ message: "origin names no default branch" });
|
|
18691
|
+
}
|
|
18692
|
+
return branch;
|
|
18693
|
+
});
|
|
18694
|
+
var checkout = gen2(function* () {
|
|
18695
|
+
const fs = yield* FileSystem;
|
|
18696
|
+
const { commonDirectory, gitDirectory, topLevel: root } = yield* gitDirectories;
|
|
18697
|
+
const isColocated = yield* fs.exists(`${root}/.jj`).pipe(orElseSucceed2(() => false));
|
|
18698
|
+
return { isColocated, isWorktree: gitDirectory !== commonDirectory, root };
|
|
18699
|
+
});
|
|
18700
|
+
var colocated = {
|
|
18701
|
+
check: map7(checkout, ({ isColocated, isWorktree }) => {
|
|
18702
|
+
const problem = isWorktree ? {
|
|
18703
|
+
byInit: false,
|
|
18704
|
+
message: "this is a git worktree, where jj would make a second repository; run `jj workspace add` in the main checkout instead"
|
|
18705
|
+
} : forInit("this checkout isn't a jj repository colocated with git");
|
|
18706
|
+
return isColocated ? undefined : problem;
|
|
18707
|
+
}),
|
|
18708
|
+
fix: gen2(function* () {
|
|
18709
|
+
const jj = yield* Jj;
|
|
18710
|
+
const { root } = yield* checkout;
|
|
18711
|
+
yield* jj.run(["git", "init", "--colocate", root]);
|
|
18712
|
+
})
|
|
18713
|
+
};
|
|
18714
|
+
var IDENTITY = ["user.name", "user.email"];
|
|
18715
|
+
var identity2 = (key) => ({
|
|
18716
|
+
check: gen2(function* () {
|
|
18717
|
+
const inJj = (yield* jjConfig(key)) !== "";
|
|
18718
|
+
const inGit = !inJj && (yield* gitConfig([key])) !== "";
|
|
18719
|
+
const problem = inGit ? forInit(`jj has no ${key}, which git has`) : {
|
|
18720
|
+
byInit: false,
|
|
18721
|
+
message: `neither jj nor git has ${key}; set it with \`jj config set --user ${key}\``
|
|
18722
|
+
};
|
|
18723
|
+
return inJj ? undefined : problem;
|
|
18724
|
+
}),
|
|
18725
|
+
fix: gen2(function* () {
|
|
18726
|
+
yield* setJjConfig(key, yield* gitConfig([key]));
|
|
18727
|
+
})
|
|
18728
|
+
});
|
|
18729
|
+
var ANONYMOUS = 'mutable() & (author_name(exact:"") | author_email(exact:""))';
|
|
18730
|
+
var authors = {
|
|
18731
|
+
check: gen2(function* () {
|
|
18732
|
+
const values = yield* forEach2(IDENTITY, jjConfig);
|
|
18733
|
+
const hasIdentity = values.every((value) => value !== "");
|
|
18734
|
+
const isAnonymous = hasIdentity && (yield* commitsAt(ANONYMOUS)).length > 0;
|
|
18735
|
+
return isAnonymous ? forInit("some mutable changes have no author") : undefined;
|
|
18736
|
+
}),
|
|
18737
|
+
fix: gen2(function* () {
|
|
18738
|
+
const jj = yield* Jj;
|
|
18739
|
+
yield* jj.run(["metaedit", "--quiet", "--update-author", "-r", ANONYMOUS]);
|
|
18740
|
+
})
|
|
18741
|
+
};
|
|
18742
|
+
var isTracked = (branch) => map7(commitsAt(`tracked_remote_bookmarks(exact:${JSON.stringify(branch)}, remote=exact:"origin")`), (commits) => commits.length > 0);
|
|
18743
|
+
var namesTrunk = (branch) => map7(jjConfig('revset-aliases."trunk()"'), (alias) => alias === trunkRevset(branch) || alias === `${branch}@origin`);
|
|
18744
|
+
var trunk = (branchOnOrigin) => ({
|
|
18745
|
+
check: gen2(function* () {
|
|
18746
|
+
const branch = yield* branchOnOrigin;
|
|
18747
|
+
return (yield* isTracked(branch)) && (yield* namesTrunk(branch)) ? undefined : forInit(`trunk() isn't ${branch}@origin, GitHub's default branch, fetched and tracked`);
|
|
18748
|
+
}),
|
|
18749
|
+
fix: gen2(function* () {
|
|
18750
|
+
const jj = yield* Jj;
|
|
18751
|
+
const branch = yield* branchOnOrigin;
|
|
18752
|
+
if ((yield* commitsAt(`present(${trunkRevset(branch)})`)).length === 0) {
|
|
18753
|
+
yield* jj.run(["git", "fetch", "--remote", "origin", "--branch", `exact:${branch}`]);
|
|
18754
|
+
}
|
|
18755
|
+
if (!(yield* isTracked(branch))) {
|
|
18756
|
+
yield* jj.run(["bookmark", "track", `exact:${branch}`, "--remote", "origin"]);
|
|
18757
|
+
}
|
|
18758
|
+
if (!(yield* namesTrunk(branch))) {
|
|
18759
|
+
yield* setJjConfig('revset-aliases."trunk()"', trunkRevset(branch));
|
|
18760
|
+
}
|
|
18761
|
+
})
|
|
18762
|
+
});
|
|
18763
|
+
var alias = {
|
|
18764
|
+
check: map7(readJjConfig("aliases.gh", { repo: true }), (value) => value === undefined ? forInit("`jj gh` isn't set up") : undefined),
|
|
18765
|
+
fix: setJjConfig("aliases.gh", ["util", "exec", "--", "jj-octo"])
|
|
18766
|
+
};
|
|
18767
|
+
var setup = map7(cached2(defaultBranch), (branch) => ({
|
|
18768
|
+
branch,
|
|
18769
|
+
steps: [...IDENTITY.map((key) => identity2(key)), authors, trunk(branch), alias]
|
|
18770
|
+
}));
|
|
18771
|
+
var runSteps = (run) => gen2(function* () {
|
|
18772
|
+
const { branch, steps } = yield* setup;
|
|
18773
|
+
const notColocated = yield* run(colocated);
|
|
18774
|
+
const found = notColocated === undefined ? yield* forEach2(steps, (step) => run(step)) : [notColocated];
|
|
18775
|
+
const problems = new Set(found.filter((problem) => problem !== undefined));
|
|
18776
|
+
return { branch, problems: [...problems] };
|
|
18777
|
+
});
|
|
18778
|
+
|
|
18779
|
+
// src/cli/gh/doctor.ts
|
|
18780
|
+
var find = ({ check }) => check.pipe(map7((problem) => problem === undefined ? undefined : advice(problem)), catch_2((error2) => succeed6(`could not check: ${error2.message}`)));
|
|
18781
|
+
var problems = map7(runSteps(find), ({ problems: found }) => found);
|
|
18782
|
+
var doctor = gen2(function* () {
|
|
18783
|
+
const found = yield* problems;
|
|
18784
|
+
if (found.length > 0) {
|
|
18785
|
+
yield* new NotSetUp({ message: found.join(`
|
|
18786
|
+
`) });
|
|
18787
|
+
}
|
|
18788
|
+
yield* error("set up for jj-octo");
|
|
18789
|
+
});
|
|
18790
|
+
var doctorCommand = make31("doctor", {}, () => asUserError(doctor)).pipe(withHelp("Check that a checkout is set up for the other commands", "Runs the checks of everything `init` sets up or requires, and changes nothing. Fails with each problem, and what fixes it."));
|
|
18791
|
+
|
|
18792
|
+
// src/cli/gh/init.ts
|
|
18793
|
+
var fixed = ({ check, fix }) => gen2(function* () {
|
|
18794
|
+
const found = yield* check;
|
|
18795
|
+
if (found?.byInit !== true) {
|
|
18796
|
+
return found?.message;
|
|
18797
|
+
}
|
|
18798
|
+
yield* fix;
|
|
18799
|
+
const problem = yield* check;
|
|
18800
|
+
return problem?.byInit === true ? `${problem.message}, though init set it up` : problem?.message;
|
|
18801
|
+
}).pipe(catch_2((error2) => succeed6(error2.message)));
|
|
18802
|
+
var init = gen2(function* () {
|
|
18803
|
+
const { branch, problems } = yield* runSteps(fixed);
|
|
18804
|
+
if (problems.length > 0) {
|
|
18805
|
+
yield* new NotSetUp({ message: problems.join(`
|
|
18806
|
+
`) });
|
|
18807
|
+
}
|
|
18808
|
+
yield* error(`set up jj, with trunk() as ${yield* branch}@origin`);
|
|
18809
|
+
});
|
|
18810
|
+
var initCommand = make31("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. Where neither has them, set them with `jj config set --user`.", "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, unless the repository has one, such as one that runs 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. Fails with what it couldn't set up, after setting up the rest. Where it can't make a colocated jj repository, such as in a git worktree, it sets up nothing else."));
|
|
18811
|
+
|
|
18702
18812
|
// src/clients/github/connection.ts
|
|
18703
18813
|
var authKey = "jj-octo.github-auth";
|
|
18704
18814
|
var ownToken = gen2(function* () {
|
|
@@ -18711,8 +18821,7 @@ var ownToken = gen2(function* () {
|
|
|
18711
18821
|
return stored === "" ? yield* fail6("no token") : make20(stored);
|
|
18712
18822
|
}).pipe(mapError3(() => new GitHubFailed({ message: "no GitHub token: set GH_TOKEN, or run `gh auth login`" })));
|
|
18713
18823
|
var gitSetting = (name) => gen2(function* () {
|
|
18714
|
-
const
|
|
18715
|
-
const value = (yield* git.read(["config", "--get-urlmatch", name, "https://github.com"]).pipe(orElseSucceed2(() => ""))).trim();
|
|
18824
|
+
const value = yield* gitConfig(["--get-urlmatch", name, "https://github.com"]).pipe(mapError3(({ message }) => new GitHubFailed({ message })));
|
|
18716
18825
|
return value === "" ? none2() : some2(value);
|
|
18717
18826
|
});
|
|
18718
18827
|
var gitProxy = gen2(function* () {
|
|
@@ -18735,8 +18844,7 @@ var gitProxy = gen2(function* () {
|
|
|
18735
18844
|
return connection;
|
|
18736
18845
|
});
|
|
18737
18846
|
var connection = gen2(function* () {
|
|
18738
|
-
const
|
|
18739
|
-
const auth = (yield* jj.read(["config", "get", authKey]).pipe(orElseSucceed2(() => ""))).trim();
|
|
18847
|
+
const auth = yield* jjConfig(authKey).pipe(mapError3(({ message }) => new GitHubFailed({ message })));
|
|
18740
18848
|
switch (auth) {
|
|
18741
18849
|
case "":
|
|
18742
18850
|
case "token": {
|
|
@@ -18864,24 +18972,7 @@ var descriptionSyncFor = ({
|
|
|
18864
18972
|
class DescriptionConflict extends TaggedError2("DescriptionConflict") {
|
|
18865
18973
|
}
|
|
18866
18974
|
var ignoreClaudeCodeFooterKey = "jj-octo.ignore-claude-code-footer";
|
|
18867
|
-
var ignoresClaudeCodeFooter =
|
|
18868
|
-
const jj = yield* Jj;
|
|
18869
|
-
const value = (yield* jj.read(["config", "get", ignoreClaudeCodeFooterKey]).pipe(orElseSucceed2(() => ""))).trim();
|
|
18870
|
-
switch (value) {
|
|
18871
|
-
case "":
|
|
18872
|
-
case "false": {
|
|
18873
|
-
return false;
|
|
18874
|
-
}
|
|
18875
|
-
case "true": {
|
|
18876
|
-
return true;
|
|
18877
|
-
}
|
|
18878
|
-
default: {
|
|
18879
|
-
return yield* new JjFailed({
|
|
18880
|
-
message: `${ignoreClaudeCodeFooterKey} is ${JSON.stringify(value)}; set it to true or false`
|
|
18881
|
-
});
|
|
18882
|
-
}
|
|
18883
|
-
}
|
|
18884
|
-
});
|
|
18975
|
+
var ignoresClaudeCodeFooter = jjFlag(ignoreClaudeCodeFooterKey);
|
|
18885
18976
|
var descriptionsOf = (commits) => commits.length === 0 ? succeed6(new Map) : readLog(commits.join(" | "), String.raw`commit_id ++ "\t" ++ description.escape_json() ++ "\n"`).pipe(map7((output) => new Map(lines2(output).map((line) => {
|
|
18886
18977
|
const [commit = "", json = '""'] = line.split("\t");
|
|
18887
18978
|
const parsed = JSON.parse(json);
|
|
@@ -18918,115 +19009,24 @@ var syncDescription = ({ bookmark, description }, pullRequest, { ignoreClaudeCod
|
|
|
18918
19009
|
})), exhaustive2);
|
|
18919
19010
|
});
|
|
18920
19011
|
|
|
18921
|
-
// src/cli/gh/stack/
|
|
18922
|
-
|
|
18923
|
-
|
|
18924
|
-
var stackContaining = (pullRequests) => gen2(function* () {
|
|
18925
|
-
const github = yield* GitHub;
|
|
18926
|
-
const found = yield* all2(pullRequests.map((pullRequest) => github.stackOf(pullRequest)), { concurrency: "unbounded" });
|
|
18927
|
-
const stacks = dedupeWith(found.flatMap((stack) => toArray(stack)), (a, b) => a.number === b.number);
|
|
18928
|
-
if (stacks.length > 1) {
|
|
18929
|
-
return yield* new SeveralStacks({
|
|
18930
|
-
message: `the pull requests belong to stacks ${stacks.map(({ number }) => `#${number}`).join(" and ")}; unstack all but one`
|
|
18931
|
-
});
|
|
18932
|
-
}
|
|
18933
|
-
return fromNullishOr(stacks[0]);
|
|
18934
|
-
});
|
|
18935
|
-
var register = (remaining, pullRequests) => gen2(function* () {
|
|
18936
|
-
const github = yield* GitHub;
|
|
18937
|
-
yield* value4(registrationFor(remaining, pullRequests)).pipe(tag3("Unchanged", () => void_3), tag3("Append", ({ pullRequests: added, stack }) => github.addToStack(stack, added).pipe(andThen2(error(`added ${added.join(", ")} to stack #${stack}`)))), tag3("Create", ({ pullRequests: all }) => github.createStack(all).pipe(flatMap4((stack) => error(`registered stack #${stack}`)))), exhaustive2);
|
|
18938
|
-
});
|
|
18939
|
-
var publish = (stack, { draft, overwriteDescriptions }) => gen2(function* () {
|
|
18940
|
-
const github = yield* GitHub;
|
|
18941
|
-
const jj = yield* Jj;
|
|
18942
|
-
const bookmarks = stack.map(({ bookmark }) => bookmark);
|
|
18943
|
-
const trunk = yield* github.defaultBranch;
|
|
18944
|
-
const bases = basesFor(bookmarks, trunk);
|
|
18945
|
-
const found = yield* all2(stack.map((change, index) => github.openPullRequest(change.bookmark).pipe(map7((pullRequest) => ({ base: bases[index] ?? trunk, change, pullRequest })))), { concurrency: "unbounded" });
|
|
18946
|
-
const targets = yield* bookmarkTargets;
|
|
18947
|
-
const pushedDescriptions = yield* descriptionsOf(bookmarks.flatMap((bookmark) => targets.get(bookmark)?.origin ?? []));
|
|
18948
|
-
const ignoreClaudeCodeFooter = yield* ignoresClaudeCodeFooter;
|
|
18949
|
-
const changes = [];
|
|
18950
|
-
for (const entry of found) {
|
|
18951
|
-
const pushed = targets.get(entry.change.bookmark)?.origin;
|
|
18952
|
-
changes.push({
|
|
18953
|
-
...entry,
|
|
18954
|
-
description: isNone2(entry.pullRequest) ? none2() : yield* syncDescription(entry.change, entry.pullRequest.value, {
|
|
18955
|
-
ignoreClaudeCodeFooter,
|
|
18956
|
-
overwriteDescriptions,
|
|
18957
|
-
pushedDescription: (pushed === undefined ? undefined : pushedDescriptions.get(pushed)) ?? ""
|
|
18958
|
-
})
|
|
18959
|
-
});
|
|
18960
|
-
}
|
|
18961
|
-
const moving = changes.flatMap(({ base, pullRequest }) => isSome2(pullRequest) && pullRequest.value.base !== base ? [pullRequest.value] : []);
|
|
18962
|
-
const wanted = changes.map(({ pullRequest }) => isSome2(pullRequest) ? some2(pullRequest.value.number) : none2());
|
|
18963
|
-
const existing = yield* stackContaining(wanted.flatMap((number) => toArray(number)));
|
|
18964
|
-
const dissolve = mustDissolve(existing, wanted, moving.length > 0);
|
|
18965
|
-
if (dissolve && isSome2(existing)) {
|
|
18966
|
-
yield* github.unstack(existing.value.number);
|
|
18967
|
-
yield* error(`dissolved stack #${existing.value.number}`);
|
|
18968
|
-
}
|
|
18969
|
-
for (const { number } of moving.filter(({ base }) => base !== trunk)) {
|
|
18970
|
-
yield* github.setBase(number, trunk);
|
|
18971
|
-
}
|
|
18972
|
-
const moved = new Set(moving.map(({ number }) => number));
|
|
18973
|
-
for (const bookmark of bookmarks.filter((name) => targets.get(name)?.tracked !== true)) {
|
|
18974
|
-
yield* jj.run(["bookmark", "track", `exact:${bookmark}`, "--remote", "origin"]);
|
|
18975
|
-
}
|
|
18976
|
-
yield* jj.run([
|
|
18977
|
-
"git",
|
|
18978
|
-
"push",
|
|
18979
|
-
...bookmarks.flatMap((bookmark) => ["--bookmark", `exact:${bookmark}`])
|
|
18980
|
-
]);
|
|
18981
|
-
const numbers = [];
|
|
18982
|
-
for (const { base, change, description, pullRequest } of changes) {
|
|
18983
|
-
if (isNone2(pullRequest)) {
|
|
18984
|
-
const created = yield* github.createPullRequest({
|
|
18985
|
-
...titleAndBody(change.description),
|
|
18986
|
-
base,
|
|
18987
|
-
draft,
|
|
18988
|
-
head: change.bookmark
|
|
18989
|
-
});
|
|
18990
|
-
yield* error(`opened #${created} for ${change.bookmark}`);
|
|
18991
|
-
numbers.push(created);
|
|
18992
|
-
} else {
|
|
18993
|
-
const { number } = pullRequest.value;
|
|
18994
|
-
if (moved.has(number) && base !== trunk) {
|
|
18995
|
-
yield* github.setBase(number, base);
|
|
18996
|
-
}
|
|
18997
|
-
if (isSome2(description)) {
|
|
18998
|
-
yield* github.setDescription(number, titleAndBody(description.value));
|
|
18999
|
-
yield* error(`updated the description of #${number}`);
|
|
19000
|
-
}
|
|
19001
|
-
numbers.push(number);
|
|
19002
|
-
}
|
|
19003
|
-
}
|
|
19004
|
-
yield* register(dissolve ? none2() : existing, numbers);
|
|
19005
|
-
});
|
|
19006
|
-
var submit = (revision, options) => gen2(function* () {
|
|
19007
|
-
yield* checkTrunk(yield* (yield* GitHub).defaultBranch);
|
|
19008
|
-
yield* publish(yield* downstackChanges(revision), options);
|
|
19009
|
-
});
|
|
19010
|
-
var republish = (revision) => gen2(function* () {
|
|
19011
|
-
if ((yield* changesIn(stackRevset(revision))).length === 0) {
|
|
19012
|
-
return;
|
|
19013
|
-
}
|
|
19014
|
-
const stack = yield* stackChanges(revision);
|
|
19015
|
-
const targets = yield* bookmarkTargets;
|
|
19016
|
-
const top = stack.findLastIndex(({ bookmark }) => targets.get(bookmark)?.origin !== undefined);
|
|
19017
|
-
if (top !== -1) {
|
|
19018
|
-
yield* publish(stack.slice(0, top + 1), { draft: true, overwriteDescriptions: false });
|
|
19019
|
-
}
|
|
19020
|
-
});
|
|
19021
|
-
|
|
19022
|
-
// src/cli/gh/sync.ts
|
|
19023
|
-
var BEHIND_ROOTS = `(${STACK_ROOTS}) ~ children(trunk())`;
|
|
19024
|
-
var bookmarksBeforeFetch = gen2(function* () {
|
|
19012
|
+
// src/cli/gh/stack/combine.ts
|
|
19013
|
+
var pushedOf = (bookmarks) => bookmarks.filter(({ remote }) => remote !== undefined);
|
|
19014
|
+
var bookmarksBeforeFetch = (revset) => gen2(function* () {
|
|
19025
19015
|
const targets = [...yield* bookmarkTargets];
|
|
19026
|
-
const
|
|
19016
|
+
const bookmarked = `(${revset}) & bookmarks()`;
|
|
19017
|
+
const bottomFirst = lines2(yield* readLog(bookmarked, String.raw`commit_id ++ "\t" ++ change_id ++ "\t" ++ current_working_copy ++ "\n"`));
|
|
19018
|
+
const children = lines2(yield* readLog(`children(${bookmarked})`, String.raw`parents.map(|p| p.commit_id()).join(",") ++ "\t" ++ change_id ++ "\n"`)).map((line) => line.split("\t"));
|
|
19027
19019
|
return bottomFirst.toReversed().flatMap((line) => {
|
|
19028
|
-
const [commit = "", change = ""] = line.split("\t");
|
|
19029
|
-
|
|
19020
|
+
const [commit = "", change = "", workingCopy] = line.split("\t");
|
|
19021
|
+
const above = children.flatMap(([parents = "", child = ""]) => parents.split(",").includes(commit) ? [child] : []);
|
|
19022
|
+
return targets.filter(([, { local }]) => local === commit).map(([name, { origin }]) => ({
|
|
19023
|
+
above,
|
|
19024
|
+
change,
|
|
19025
|
+
editing: workingCopy === "true",
|
|
19026
|
+
local: commit,
|
|
19027
|
+
name,
|
|
19028
|
+
remote: origin
|
|
19029
|
+
}));
|
|
19030
19030
|
});
|
|
19031
19031
|
});
|
|
19032
19032
|
var localCommit = (change, remote, fetched) => gen2(function* () {
|
|
@@ -19037,22 +19037,31 @@ var localCommit = (change, remote, fetched) => gen2(function* () {
|
|
|
19037
19037
|
}
|
|
19038
19038
|
return local;
|
|
19039
19039
|
});
|
|
19040
|
-
var combine4 = ({ change, local: localBefore, name, remote }, fetched) => gen2(function* () {
|
|
19040
|
+
var combine4 = ({ above, change, editing, local: localBefore, name, remote }, fetched) => gen2(function* () {
|
|
19041
19041
|
const jj = yield* Jj;
|
|
19042
19042
|
if (remote === undefined || fetched === undefined || fetched === remote) {
|
|
19043
19043
|
return true;
|
|
19044
19044
|
}
|
|
19045
19045
|
const [local, ...others] = yield* localCommit(change, remote, fetched);
|
|
19046
19046
|
if (local === undefined) {
|
|
19047
|
+
if (editing) {
|
|
19048
|
+
yield* jj.run(["edit", fetched]);
|
|
19049
|
+
}
|
|
19050
|
+
const moved = `(${anyOf(above.map((child) => `change_id(${child})`))}) ~ children(${fetched})`;
|
|
19051
|
+
if ((yield* commitsAt(moved)).length > 0) {
|
|
19052
|
+
yield* jj.run(["rebase", "--source", moved, "--destination", fetched]);
|
|
19053
|
+
yield* error(`took GitHub's rewritten ${name}, and moved what was on it there`);
|
|
19054
|
+
}
|
|
19047
19055
|
return true;
|
|
19048
19056
|
}
|
|
19049
19057
|
if (others.length > 0) {
|
|
19050
19058
|
return false;
|
|
19051
19059
|
}
|
|
19052
19060
|
const keepLocal = jj.run(["bookmark", "set", name, "-r", local, "--allow-backwards"]);
|
|
19061
|
+
const held = `::(bookmarks() ~ ${bookmarkRevset(name)}) ~ ::${remote}`;
|
|
19053
19062
|
const dropCopies = jj.run([
|
|
19054
19063
|
"abandon",
|
|
19055
|
-
`((::${fetched} | ::${remote}) & mutable()) ~ ::${bookmarkRevset(name)}`
|
|
19064
|
+
`((::${fetched} | ::${remote}) & mutable()) ~ ::${bookmarkRevset(name)} ~ (${held})`
|
|
19056
19065
|
]);
|
|
19057
19066
|
if ((yield* commitsAt(`${remote} & ::${fetched}`)).length > 0) {
|
|
19058
19067
|
yield* keepLocal;
|
|
@@ -19070,6 +19079,9 @@ var combine4 = ({ change, local: localBefore, name, remote }, fetched) => gen2(f
|
|
|
19070
19079
|
}
|
|
19071
19080
|
if (localBefore === remote) {
|
|
19072
19081
|
yield* jj.run(["rebase", "--source", `children(${local})`, "--destination", fetched]);
|
|
19082
|
+
if (editing) {
|
|
19083
|
+
yield* jj.run(["edit", fetched]);
|
|
19084
|
+
}
|
|
19073
19085
|
yield* jj.run(["abandon", local]);
|
|
19074
19086
|
yield* error(`took GitHub's rewritten ${name}, which wasn't changed locally`);
|
|
19075
19087
|
return true;
|
|
@@ -19084,6 +19096,9 @@ var combine4 = ({ change, local: localBefore, name, remote }, fetched) => gen2(f
|
|
|
19084
19096
|
return false;
|
|
19085
19097
|
});
|
|
19086
19098
|
|
|
19099
|
+
// src/cli/gh/stack/reconcile.ts
|
|
19100
|
+
var BEHIND_ROOTS = `(${STACK_ROOTS}) ~ children(trunk())`;
|
|
19101
|
+
|
|
19087
19102
|
class NotCombined extends TaggedError2("NotCombined") {
|
|
19088
19103
|
}
|
|
19089
19104
|
var rebaseStack = (root, required) => gen2(function* () {
|
|
@@ -19103,8 +19118,8 @@ var rebaseStack = (root, required) => gen2(function* () {
|
|
|
19103
19118
|
String.raw`id ++ "\n"`
|
|
19104
19119
|
]));
|
|
19105
19120
|
yield* jj.run(["rebase", "--source", root, "--destination", "trunk()", "--skip-emptied"]);
|
|
19106
|
-
const rebased =
|
|
19107
|
-
const untouched =
|
|
19121
|
+
const rebased = anyOf(changes.map((change) => `change_id(${change})`));
|
|
19122
|
+
const untouched = anyOf(conflictedCommits);
|
|
19108
19123
|
const conflictedNow = yield* changeIdsAt(`(conflicts() & (${rebased})) ~ (${untouched})`);
|
|
19109
19124
|
const mayRestore = !changes.some((change) => required.includes(change));
|
|
19110
19125
|
if (mayRestore && conflictedNow.some((change) => !conflicted.includes(change))) {
|
|
@@ -19112,23 +19127,49 @@ var rebaseStack = (root, required) => gen2(function* () {
|
|
|
19112
19127
|
yield* error(`left "${name}" and the changes on it where they were: they conflict with the trunk`);
|
|
19113
19128
|
}
|
|
19114
19129
|
});
|
|
19115
|
-
var
|
|
19130
|
+
var notMerged = (merged) => (name) => merged.every(({ bookmark }) => bookmark !== name);
|
|
19131
|
+
var mergedHeadsOf = (bookmarks) => gen2(function* () {
|
|
19116
19132
|
const github = yield* GitHub;
|
|
19117
|
-
const
|
|
19118
|
-
|
|
19133
|
+
const pushed = pushedOf(bookmarks);
|
|
19134
|
+
const heads = yield* forEach2(pushed, ({ name }) => github.mergedHeads(name), {
|
|
19135
|
+
concurrency: "unbounded"
|
|
19136
|
+
});
|
|
19137
|
+
return Object.fromEntries(pushed.map(({ name }, index) => [name, heads[index] ?? []]));
|
|
19119
19138
|
});
|
|
19120
|
-
var
|
|
19139
|
+
var mergedIn = (bookmarks, heads) => gen2(function* () {
|
|
19121
19140
|
const jj = yield* Jj;
|
|
19122
|
-
const
|
|
19123
|
-
|
|
19124
|
-
|
|
19125
|
-
|
|
19126
|
-
|
|
19127
|
-
|
|
19128
|
-
|
|
19129
|
-
|
|
19141
|
+
const merged = yield* forEach2(bookmarks, ({ change, local, name, remote }) => gen2(function* () {
|
|
19142
|
+
const merges = heads[name] ?? [];
|
|
19143
|
+
const rebasedOnly = remote !== undefined && merges.includes(remote) && (yield* jj.read(["interdiff", "--from", remote, "--to", local, "--summary"])).trim() === "";
|
|
19144
|
+
return merges.includes(local) || rebasedOnly ? [{ bookmark: name, changeId: change }] : [];
|
|
19145
|
+
}));
|
|
19146
|
+
return merged.flat();
|
|
19147
|
+
});
|
|
19148
|
+
var mergedOnGitHub = (bookmarks) => flatMap4(mergedHeadsOf(bookmarks), (heads) => mergedIn(bookmarks, heads));
|
|
19149
|
+
var forget = (names) => gen2(function* () {
|
|
19150
|
+
if (names.length > 0) {
|
|
19151
|
+
const jj = yield* Jj;
|
|
19152
|
+
yield* jj.run([
|
|
19153
|
+
"bookmark",
|
|
19154
|
+
"forget",
|
|
19155
|
+
"--include-remotes",
|
|
19156
|
+
...names.map((name) => `exact:${name}`)
|
|
19157
|
+
]);
|
|
19130
19158
|
}
|
|
19131
|
-
|
|
19159
|
+
});
|
|
19160
|
+
var reconcile = ({
|
|
19161
|
+
before,
|
|
19162
|
+
merged,
|
|
19163
|
+
scope
|
|
19164
|
+
}) => gen2(function* () {
|
|
19165
|
+
const jj = yield* Jj;
|
|
19166
|
+
const branches = scope === "every stack" ? [] : [scope.trunk, ...pushedOf(before).map(({ name }) => name)].flatMap((branch) => [
|
|
19167
|
+
"--branch",
|
|
19168
|
+
`exact:${branch}`
|
|
19169
|
+
]);
|
|
19170
|
+
yield* jj.run(["git", "fetch", ...branches]);
|
|
19171
|
+
yield* forget(merged.map(({ bookmark }) => bookmark));
|
|
19172
|
+
const mergedRevset = `(${anyOf(merged.map(({ changeId }) => `change_id(${changeId})`))}) & mutable()`;
|
|
19132
19173
|
const required = yield* changeIdsAt(`(${mergedRevset}):: ~ (${mergedRevset})`);
|
|
19133
19174
|
if (merged.length > 0) {
|
|
19134
19175
|
yield* jj.run(["abandon", mergedRevset]);
|
|
@@ -19136,18 +19177,20 @@ var sync4 = (known = []) => gen2(function* () {
|
|
|
19136
19177
|
}
|
|
19137
19178
|
const fetched = yield* bookmarkTargets;
|
|
19138
19179
|
const notCombined = [];
|
|
19139
|
-
for (const bookmark of before.filter(({ name }) => merged
|
|
19180
|
+
for (const bookmark of before.filter(({ name }) => notMerged(merged)(name))) {
|
|
19140
19181
|
if (!(yield* combine4(bookmark, fetched.get(bookmark.name)?.origin))) {
|
|
19141
19182
|
notCombined.push(bookmark.name);
|
|
19142
19183
|
}
|
|
19143
19184
|
}
|
|
19144
|
-
|
|
19185
|
+
const roots = scope === "every stack" ? BEHIND_ROOTS : `(${BEHIND_ROOTS}) & ::(${anyOf([
|
|
19186
|
+
...scope.bookmarks.map((name) => bookmarkRevset(name)),
|
|
19187
|
+
...scope.unnamed.map((change) => `change_id(${change})`)
|
|
19188
|
+
])})`;
|
|
19189
|
+
for (const root of yield* commitsAt(roots)) {
|
|
19145
19190
|
yield* rebaseStack(root, required);
|
|
19146
19191
|
}
|
|
19147
|
-
const
|
|
19148
|
-
|
|
19149
|
-
yield* jj.run(["bookmark", "forget", "--include-remotes", `exact:${name}`]);
|
|
19150
|
-
}
|
|
19192
|
+
const onStacks = new Set(scope === "every stack" ? before.map(({ name }) => name) : scope.bookmarks);
|
|
19193
|
+
yield* forget((yield* bookmarksAt("::trunk()")).filter((name) => onStacks.has(name)));
|
|
19151
19194
|
if (notCombined.length > 0) {
|
|
19152
19195
|
yield* new NotCombined({
|
|
19153
19196
|
message: [
|
|
@@ -19159,10 +19202,203 @@ var sync4 = (known = []) => gen2(function* () {
|
|
|
19159
19202
|
});
|
|
19160
19203
|
}
|
|
19161
19204
|
});
|
|
19205
|
+
|
|
19206
|
+
// src/cli/gh/stack/sync-stack.ts
|
|
19207
|
+
class BranchesDeleted extends TaggedError2("BranchesDeleted") {
|
|
19208
|
+
}
|
|
19209
|
+
var whyDeleted = (heads, { remote }) => {
|
|
19210
|
+
if (remote !== undefined && heads.includes(remote)) {
|
|
19211
|
+
return "its pushed commit merged, and the change was edited since";
|
|
19212
|
+
}
|
|
19213
|
+
return heads.length > 0 ? "a pull request from it merged, but not the commit pushed for this change" : "no pull request from it merged";
|
|
19214
|
+
};
|
|
19215
|
+
var changedOnGitHub = (bookmarks) => gen2(function* () {
|
|
19216
|
+
const pushed = pushedOf(bookmarks);
|
|
19217
|
+
if (pushed.length === 0) {
|
|
19218
|
+
return { changed: [], deleted: [] };
|
|
19219
|
+
}
|
|
19220
|
+
const git = yield* Git;
|
|
19221
|
+
const gitDirectory = (yield* (yield* Jj).read(["git", "root"])).trim();
|
|
19222
|
+
const output = yield* git.read([
|
|
19223
|
+
"--git-dir",
|
|
19224
|
+
gitDirectory,
|
|
19225
|
+
"ls-remote",
|
|
19226
|
+
"origin",
|
|
19227
|
+
...pushed.map(({ name }) => `refs/heads/${name}`)
|
|
19228
|
+
]);
|
|
19229
|
+
const onGitHub = new Map(lines2(output).map((line) => {
|
|
19230
|
+
const [commit = "", ref = ""] = line.split("\t");
|
|
19231
|
+
return [ref.replace(/^refs\/heads\//u, ""), commit];
|
|
19232
|
+
}));
|
|
19233
|
+
return {
|
|
19234
|
+
changed: pushed.filter(({ name, remote }) => onGitHub.has(name) && onGitHub.get(name) !== remote).map(({ name }) => name),
|
|
19235
|
+
deleted: pushed.filter(({ name }) => !onGitHub.has(name))
|
|
19236
|
+
};
|
|
19237
|
+
});
|
|
19238
|
+
var syncStack = (revision, trunk) => gen2(function* () {
|
|
19239
|
+
const downstack = downstackRevset(revision);
|
|
19240
|
+
const before = yield* bookmarksBeforeFetch(downstack);
|
|
19241
|
+
const [heads, onGitHub] = yield* all2([mergedHeadsOf(before), changedOnGitHub(before)], {
|
|
19242
|
+
concurrency: "unbounded"
|
|
19243
|
+
});
|
|
19244
|
+
const merged = yield* mergedIn(before, heads);
|
|
19245
|
+
const changed = onGitHub.changed.filter(notMerged(merged));
|
|
19246
|
+
const deleted = onGitHub.deleted.filter(({ name }) => notMerged(merged)(name));
|
|
19247
|
+
if (deleted.length > 0) {
|
|
19248
|
+
return yield* new BranchesDeleted({
|
|
19249
|
+
message: [
|
|
19250
|
+
"these branches were deleted on GitHub, so nothing was changed:",
|
|
19251
|
+
...deleted.map((bookmark) => `${bookmark.name}: ${whyDeleted(heads[bookmark.name] ?? [], bookmark)}`),
|
|
19252
|
+
"to push a change again, run `jj bookmark forget --include-remotes <name>`, then `jj bookmark create <name> -r <change>`; to drop one, run `jj abandon <change>`"
|
|
19253
|
+
].join(`
|
|
19254
|
+
`)
|
|
19255
|
+
});
|
|
19256
|
+
}
|
|
19257
|
+
const found = [
|
|
19258
|
+
...merged.map(({ bookmark }) => `${bookmark} merged`),
|
|
19259
|
+
...changed.map((name) => `${name} changed`)
|
|
19260
|
+
];
|
|
19261
|
+
if (found.length === 0) {
|
|
19262
|
+
return false;
|
|
19263
|
+
}
|
|
19264
|
+
yield* error(`on GitHub, ${found.join(", ")}; syncing the stack first`);
|
|
19265
|
+
const whole = stackRevset(revision);
|
|
19266
|
+
yield* reconcile({
|
|
19267
|
+
before,
|
|
19268
|
+
merged,
|
|
19269
|
+
scope: {
|
|
19270
|
+
bookmarks: yield* bookmarksAt(whole),
|
|
19271
|
+
trunk,
|
|
19272
|
+
unnamed: yield* changeIdsAt(`((${whole}) | (@ & children(${whole}))) ~ bookmarks()`)
|
|
19273
|
+
}
|
|
19274
|
+
});
|
|
19275
|
+
return true;
|
|
19276
|
+
});
|
|
19277
|
+
|
|
19278
|
+
// src/cli/gh/stack/publish.ts
|
|
19279
|
+
class SeveralStacks extends TaggedError2("SeveralStacks") {
|
|
19280
|
+
}
|
|
19281
|
+
var stackContaining = (pullRequests) => gen2(function* () {
|
|
19282
|
+
const github = yield* GitHub;
|
|
19283
|
+
const found = yield* all2(pullRequests.map((pullRequest) => github.stackOf(pullRequest)), { concurrency: "unbounded" });
|
|
19284
|
+
const stacks = dedupeWith(found.flatMap((stack) => toArray(stack)), (a, b) => a.number === b.number);
|
|
19285
|
+
if (stacks.length > 1) {
|
|
19286
|
+
return yield* new SeveralStacks({
|
|
19287
|
+
message: `the pull requests belong to stacks ${stacks.map(({ number }) => `#${number}`).join(" and ")}; unstack all but one`
|
|
19288
|
+
});
|
|
19289
|
+
}
|
|
19290
|
+
return fromNullishOr(stacks[0]);
|
|
19291
|
+
});
|
|
19292
|
+
var register = (remaining, pullRequests) => gen2(function* () {
|
|
19293
|
+
const github = yield* GitHub;
|
|
19294
|
+
yield* value4(registrationFor(remaining, pullRequests)).pipe(tag3("Unchanged", () => void_3), tag3("Append", ({ pullRequests: added, stack }) => github.addToStack(stack, added).pipe(andThen2(error(`added ${added.join(", ")} to stack #${stack}`)))), tag3("Create", ({ pullRequests: all }) => github.createStack(all).pipe(flatMap4((stack) => error(`registered stack #${stack}`)))), exhaustive2);
|
|
19295
|
+
});
|
|
19296
|
+
var publish = (stack, { draft, overwriteDescriptions, trunk }) => gen2(function* () {
|
|
19297
|
+
const github = yield* GitHub;
|
|
19298
|
+
const jj = yield* Jj;
|
|
19299
|
+
const bookmarks = stack.map(({ bookmark }) => bookmark);
|
|
19300
|
+
const bases = basesFor(bookmarks, trunk);
|
|
19301
|
+
const found = yield* all2(stack.map((change, index) => github.openPullRequest(change.bookmark).pipe(map7((pullRequest) => ({ base: bases[index] ?? trunk, change, pullRequest })))), { concurrency: "unbounded" });
|
|
19302
|
+
const targets = yield* bookmarkTargets;
|
|
19303
|
+
const pushedDescriptions = yield* descriptionsOf(bookmarks.flatMap((bookmark) => targets.get(bookmark)?.origin ?? []));
|
|
19304
|
+
const ignoreClaudeCodeFooter = yield* ignoresClaudeCodeFooter;
|
|
19305
|
+
const changes = [];
|
|
19306
|
+
for (const entry of found) {
|
|
19307
|
+
const pushed = targets.get(entry.change.bookmark)?.origin;
|
|
19308
|
+
changes.push({
|
|
19309
|
+
...entry,
|
|
19310
|
+
description: isNone2(entry.pullRequest) ? none2() : yield* syncDescription(entry.change, entry.pullRequest.value, {
|
|
19311
|
+
ignoreClaudeCodeFooter,
|
|
19312
|
+
overwriteDescriptions,
|
|
19313
|
+
pushedDescription: (pushed === undefined ? undefined : pushedDescriptions.get(pushed)) ?? ""
|
|
19314
|
+
})
|
|
19315
|
+
});
|
|
19316
|
+
}
|
|
19317
|
+
const moving = changes.flatMap(({ base, pullRequest }) => isSome2(pullRequest) && pullRequest.value.base !== base ? [pullRequest.value] : []);
|
|
19318
|
+
const wanted = changes.map(({ pullRequest }) => isSome2(pullRequest) ? some2(pullRequest.value.number) : none2());
|
|
19319
|
+
const existing = yield* stackContaining(wanted.flatMap((number) => toArray(number)));
|
|
19320
|
+
const dissolve = mustDissolve(existing, wanted, moving.length > 0);
|
|
19321
|
+
if (dissolve && isSome2(existing)) {
|
|
19322
|
+
yield* github.unstack(existing.value.number);
|
|
19323
|
+
yield* error(`dissolved stack #${existing.value.number}`);
|
|
19324
|
+
}
|
|
19325
|
+
for (const { number } of moving.filter(({ base }) => base !== trunk)) {
|
|
19326
|
+
yield* github.setBase(number, trunk);
|
|
19327
|
+
}
|
|
19328
|
+
const moved = new Set(moving.map(({ number }) => number));
|
|
19329
|
+
for (const bookmark of bookmarks.filter((name) => targets.get(name)?.tracked !== true)) {
|
|
19330
|
+
yield* jj.run(["bookmark", "track", `exact:${bookmark}`, "--remote", "origin"]);
|
|
19331
|
+
}
|
|
19332
|
+
yield* jj.run([
|
|
19333
|
+
"git",
|
|
19334
|
+
"push",
|
|
19335
|
+
...bookmarks.flatMap((bookmark) => ["--bookmark", `exact:${bookmark}`])
|
|
19336
|
+
]);
|
|
19337
|
+
const numbers = [];
|
|
19338
|
+
for (const { base, change, description, pullRequest } of changes) {
|
|
19339
|
+
if (isNone2(pullRequest)) {
|
|
19340
|
+
const created = yield* github.createPullRequest({
|
|
19341
|
+
...titleAndBody(change.description),
|
|
19342
|
+
base,
|
|
19343
|
+
draft,
|
|
19344
|
+
head: change.bookmark
|
|
19345
|
+
});
|
|
19346
|
+
yield* error(`opened #${created} for ${change.bookmark}`);
|
|
19347
|
+
numbers.push(created);
|
|
19348
|
+
} else {
|
|
19349
|
+
const { number } = pullRequest.value;
|
|
19350
|
+
if (moved.has(number) && base !== trunk) {
|
|
19351
|
+
yield* github.setBase(number, base);
|
|
19352
|
+
}
|
|
19353
|
+
if (isSome2(description)) {
|
|
19354
|
+
yield* github.setDescription(number, titleAndBody(description.value));
|
|
19355
|
+
yield* error(`updated the description of #${number}`);
|
|
19356
|
+
}
|
|
19357
|
+
numbers.push(number);
|
|
19358
|
+
}
|
|
19359
|
+
}
|
|
19360
|
+
yield* register(dissolve ? none2() : existing, numbers);
|
|
19361
|
+
});
|
|
19362
|
+
var submit = (revision, options) => gen2(function* () {
|
|
19363
|
+
const trunk = yield* checkedTrunk;
|
|
19364
|
+
const downstack = yield* downstackChanges(revision);
|
|
19365
|
+
if (!(yield* syncStack(revision, trunk))) {
|
|
19366
|
+
yield* publish(downstack, { ...options, trunk });
|
|
19367
|
+
return;
|
|
19368
|
+
}
|
|
19369
|
+
const left = yield* bookmarksAt(anyOf(downstack.map(({ bookmark }) => bookmarkRevset(bookmark))));
|
|
19370
|
+
const top = downstack.findLast(({ bookmark }) => left.includes(bookmark));
|
|
19371
|
+
if (top === undefined) {
|
|
19372
|
+
yield* error(`nothing left to submit: no bookmark of the stack is left after the sync`);
|
|
19373
|
+
return;
|
|
19374
|
+
}
|
|
19375
|
+
yield* publish(yield* downstackChanges(bookmarkRevset(top.bookmark)), { ...options, trunk });
|
|
19376
|
+
});
|
|
19377
|
+
var republish = (revision, trunk) => gen2(function* () {
|
|
19378
|
+
if ((yield* changesIn(stackRevset(revision))).length === 0) {
|
|
19379
|
+
return;
|
|
19380
|
+
}
|
|
19381
|
+
const stack = yield* stackChanges(revision);
|
|
19382
|
+
const targets = yield* bookmarkTargets;
|
|
19383
|
+
const top = stack.findLastIndex(({ bookmark }) => targets.get(bookmark)?.origin !== undefined);
|
|
19384
|
+
if (top !== -1) {
|
|
19385
|
+
yield* publish(stack.slice(0, top + 1), { draft: true, overwriteDescriptions: false, trunk });
|
|
19386
|
+
}
|
|
19387
|
+
});
|
|
19388
|
+
|
|
19389
|
+
// src/cli/gh/sync.ts
|
|
19390
|
+
var sync4 = (known = []) => gen2(function* () {
|
|
19391
|
+
const before = yield* bookmarksBeforeFetch("mutable()");
|
|
19392
|
+
const merged = [
|
|
19393
|
+
...known,
|
|
19394
|
+
...yield* mergedOnGitHub(before.filter(({ name }) => notMerged(known)(name)))
|
|
19395
|
+
];
|
|
19396
|
+
yield* reconcile({ before, merged, scope: "every stack" });
|
|
19397
|
+
});
|
|
19162
19398
|
var syncCommand = make31("sync", {}, () => asUserError(gen2(function* () {
|
|
19163
|
-
yield*
|
|
19399
|
+
yield* checkedTrunk;
|
|
19164
19400
|
yield* sync4();
|
|
19165
|
-
}).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 whose pull request GitHub merged is abandoned and its bookmark forgotten, unless it was edited after the merge.", "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. The rest of a stack whose changes merged is rebased even where it conflicts, and the conflict is recorded in the change.", "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
|
|
19401
|
+
}).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 whose pull request GitHub merged is abandoned and its bookmark forgotten, unless it was edited after the merge.", "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. The rest of a stack whose changes merged is rebased even where it conflicts, and the conflict is recorded in the change.", "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-octo init` sets.", "Pushes nothing. Run `jj gh submit` to update the pull requests of a change and the ones below it."));
|
|
19166
19402
|
|
|
19167
19403
|
// src/cli/gh/merge.ts
|
|
19168
19404
|
class NotMergeable extends TaggedError2("NotMergeable") {
|
|
@@ -19196,8 +19432,7 @@ var mergeOnGitHub = ({ base, number }, trunk) => gen2(function* () {
|
|
|
19196
19432
|
return "merged";
|
|
19197
19433
|
});
|
|
19198
19434
|
var merge5 = (revision) => gen2(function* () {
|
|
19199
|
-
const trunk = yield*
|
|
19200
|
-
yield* checkTrunk(trunk);
|
|
19435
|
+
const trunk = yield* checkedTrunk;
|
|
19201
19436
|
const changes = yield* downstackChanges(revision);
|
|
19202
19437
|
const top = changes.at(-1)?.bookmark ?? "";
|
|
19203
19438
|
const targets = yield* bookmarkTargets;
|
|
@@ -19216,7 +19451,7 @@ var merge5 = (revision) => gen2(function* () {
|
|
|
19216
19451
|
yield* sync4(changes);
|
|
19217
19452
|
for (const bookmark of above) {
|
|
19218
19453
|
if ((yield* commitsAt(bookmarkRevset(bookmark))).length > 0) {
|
|
19219
|
-
yield* republish(bookmarkRevset(bookmark));
|
|
19454
|
+
yield* republish(bookmarkRevset(bookmark), trunk);
|
|
19220
19455
|
break;
|
|
19221
19456
|
}
|
|
19222
19457
|
}
|
|
@@ -19229,10 +19464,17 @@ var submitCommand = make31("submit", {
|
|
|
19229
19464
|
overwriteDescriptions: Boolean6("overwrite-descriptions").pipe(withDefault5(false), withDescription2("Where a description changed both locally and on GitHub, send the local one instead of merging the two")),
|
|
19230
19465
|
publish: Boolean6("publish").pipe(withDefault5(false), withDescription2("Open new pull requests ready for review, instead of as drafts")),
|
|
19231
19466
|
revision: revisionFlag("The top change to submit")
|
|
19232
|
-
}, ({ 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."));
|
|
19467
|
+
}, ({ 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.", "First, where any of those changes' pull requests merged or any of their branches changed on GitHub, syncs their stack as `jj gh sync` does, and leaves other stacks alone. That drops the merged changes, takes what GitHub added to the branches, and rebases the rest onto the trunk, so the push doesn't undo GitHub's work.", "Stops before changing anything where one of their branches was deleted on GitHub and their change doesn't count as merged: no pull request from it merged, another commit merged, or the change was edited since. It says how to push the change again, or drop it.", "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."));
|
|
19233
19468
|
|
|
19234
19469
|
// src/cli/gh/command.ts
|
|
19235
|
-
var jjGhCommand = make31("jj gh").pipe(withSubcommands([
|
|
19470
|
+
var jjGhCommand = make31("jj gh").pipe(withSubcommands([
|
|
19471
|
+
doctorCommand,
|
|
19472
|
+
initCommand,
|
|
19473
|
+
logCommand,
|
|
19474
|
+
mergeCommand,
|
|
19475
|
+
submitCommand,
|
|
19476
|
+
syncCommand
|
|
19477
|
+
]), 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`."));
|
|
19236
19478
|
|
|
19237
19479
|
// src/main.ts
|
|
19238
19480
|
runMain2(run5(jjGhCommand, { version: package_default.version }).pipe(provide3([GitLive, JjLive]), provide3(layer13({
|