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.
Files changed (3) hide show
  1. package/README.md +2 -12
  2. package/dist/jj-octo.js +496 -254
  3. 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.5.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 git = yield* Git;
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 jj = yield* Jj;
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 = gen2(function* () {
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/publish.ts
18922
- class SeveralStacks extends TaggedError2("SeveralStacks") {
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 bottomFirst = lines2(yield* readLog("mutable() & bookmarks()", String.raw`commit_id ++ "\t" ++ change_id ++ "\n"`));
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
- return targets.filter(([, { local }]) => local === commit).map(([name, { origin }]) => ({ change, local: commit, name, remote: origin }));
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 = ["none()", ...changes.map((change) => `change_id(${change})`)].join(" | ");
19107
- const untouched = ["none()", ...conflictedCommits].join(" | ");
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 mergedOnGitHub = (bookmarks) => gen2(function* () {
19130
+ var notMerged = (merged) => (name) => merged.every(({ bookmark }) => bookmark !== name);
19131
+ var mergedHeadsOf = (bookmarks) => gen2(function* () {
19116
19132
  const github = yield* GitHub;
19117
- const merged = yield* forEach2(bookmarks.filter(({ remote }) => remote !== undefined), ({ change, local, name }) => github.mergedHeads(name).pipe(map7((heads) => heads.includes(local) ? [{ bookmark: name, changeId: change }] : [])), { concurrency: "unbounded" });
19118
- return merged.flat();
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 sync4 = (known = []) => gen2(function* () {
19139
+ var mergedIn = (bookmarks, heads) => gen2(function* () {
19121
19140
  const jj = yield* Jj;
19122
- const before = yield* bookmarksBeforeFetch;
19123
- const merged = [
19124
- ...known,
19125
- ...yield* mergedOnGitHub(before.filter(({ name }) => known.every(({ bookmark }) => bookmark !== name)))
19126
- ];
19127
- yield* jj.run(["git", "fetch"]);
19128
- for (const { bookmark } of merged) {
19129
- yield* jj.run(["bookmark", "forget", "--include-remotes", `exact:${bookmark}`]);
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
- const mergedRevset = `(${["none()", ...merged.map(({ changeId }) => `change_id(${changeId})`)].join(" | ")}) & mutable()`;
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.every((change) => change.bookmark !== name))) {
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
- for (const root of yield* commitsAt(BEHIND_ROOTS)) {
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 onTrunk = (yield* bookmarksAt("::trunk()")).filter((name) => before.some((bookmark) => bookmark.name === name));
19148
- for (const name of onTrunk) {
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* checkTrunk(yield* (yield* GitHub).defaultBranch);
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 gh init` sets.", "Pushes nothing. Run `jj gh submit` to update the pull requests of a change and the ones below it."));
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* (yield* GitHub).defaultBranch;
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([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`."));
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({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jj-octo",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "A jj subcommand for GitHub that supports stacked pull requests and works in Claude Code cloud sessions",
5
5
  "license": "MIT",
6
6
  "repository": {