jj-octo 0.6.0 → 0.7.1

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 (2) hide show
  1. package/dist/jj-octo.js +415 -257
  2. package/package.json +1 -1
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.6.0",
18242
+ version: "0.7.1",
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: {
@@ -18377,20 +18377,26 @@ class Jj extends Service()("jj-octo/Jj") {
18377
18377
  var failed = (args, detail) => new JjFailed({
18378
18378
  message: detail.trim() === "" ? `jj ${args.join(" ")} failed` : detail.trim().replace(/^Error: /u, "")
18379
18379
  });
18380
- var jjLayer = ({ cwd, env, quiet = false } = {}) => effect(Jj, gen2(function* () {
18380
+ var jjLayer = ({ cwd, env, printed } = {}) => effect(Jj, gen2(function* () {
18381
18381
  const spawner = yield* ChildProcessSpawner;
18382
18382
  const location2 = { cwd, env, extendEnv: true };
18383
- const output = quiet ? "ignore" : "inherit";
18384
- const read = (args) => scoped2(gen2(function* () {
18383
+ const captured = (args) => scoped2(gen2(function* () {
18385
18384
  const handle = yield* spawner.spawn(make15("jj", args, location2));
18386
- const [stdout, stderr, code] = yield* all2([
18385
+ return yield* all2([
18387
18386
  mkString(decodeText(handle.stdout)),
18388
18387
  mkString(decodeText(handle.stderr)),
18389
18388
  handle.exitCode
18390
18389
  ], { concurrency: "unbounded" });
18391
- return code === 0 ? stdout : yield* failed(args, stderr);
18392
18390
  })).pipe(catchTag2("PlatformError", (error) => fail6(failed(args, error.message))));
18393
- const run = (args) => spawner.exitCode(make15("jj", args, { ...location2, stderr: output, stdout: output })).pipe(catchTag2("PlatformError", (error) => fail6(failed(args, error.message))), flatMap4((code) => code === 0 ? void_3 : fail6(failed(args, ""))));
18391
+ const read = (args) => flatMap4(captured(args), ([stdout, stderr, code]) => code === 0 ? succeed6(stdout) : fail6(failed(args, stderr)));
18392
+ const run = (args) => printed === undefined ? spawner.exitCode(make15("jj", args, {
18393
+ ...location2,
18394
+ stderr: "inherit",
18395
+ stdout: "inherit"
18396
+ })).pipe(catchTag2("PlatformError", (error) => fail6(failed(args, error.message))), flatMap4((code) => code === 0 ? void_3 : fail6(failed(args, "")))) : flatMap4(captured(args), ([stdout, stderr, code]) => {
18397
+ printed(stdout + stderr);
18398
+ return code === 0 ? void_3 : fail6(failed(args, stderr));
18399
+ });
18394
18400
  return { read, run };
18395
18401
  }));
18396
18402
  var JjLive = jjLayer();
@@ -18439,6 +18445,7 @@ var SCRATCH = '(working_copies() & empty() & description(exact:""))';
18439
18445
  var stackRevset = (revision) => `(reachable(${revision}, mutable()) & (${STACK_ROOTS})::) ~ ${SCRATCH}`;
18440
18446
  var downstackRevset = (revision) => `::(${revision}) & (${stackRevset(revision)})`;
18441
18447
  var upstackRevset = (revision) => `(${revision})+:: & (${stackRevset(revision)})`;
18448
+ var anyOf = (revsets) => ["none()", ...revsets].join(" | ");
18442
18449
  var bookmarkRevset = (bookmark) => `bookmarks(exact:"${bookmark}")`;
18443
18450
  var lines2 = (output) => output.split(`
18444
18451
  `).filter((line) => line !== "");
@@ -18544,6 +18551,121 @@ var chain = (revset, revision) => gen2(function* () {
18544
18551
  var stackChanges = (revision) => chain(stackRevset(revision), revision);
18545
18552
  var downstackChanges = (revision) => chain(downstackRevset(revision), revision);
18546
18553
 
18554
+ // src/clients/github/openapi.ts
18555
+ var responseOf = () => (schema) => schema;
18556
+ var bodyOf = (body) => bodyJsonUnsafe(body);
18557
+
18558
+ // src/clients/github/types.ts
18559
+ var PullRequestNumberSchema = Number6.pipe(brand("PullRequestNumber"));
18560
+ var StackNumberSchema = Number6.pipe(brand("StackNumber"));
18561
+
18562
+ // src/clients/github/client.ts
18563
+ class GitHubFailed extends TaggedError2("GitHubFailed") {
18564
+ }
18565
+
18566
+ class GitHub extends Service()("jj-octo/GitHub") {
18567
+ }
18568
+ var CreatedPullRequest = responseOf()(Struct({ number: PullRequestNumberSchema }));
18569
+ var CreatedStack = responseOf()(Struct({ number: StackNumberSchema }));
18570
+ var PullRequests = responseOf()(ArraySchema(Struct({
18571
+ base: Struct({ ref: String4 }),
18572
+ body: NullOr(String4),
18573
+ draft: optionalKey2(Boolean3),
18574
+ number: PullRequestNumberSchema,
18575
+ title: String4
18576
+ })));
18577
+ var ClosedPullRequests = responseOf()(ArraySchema(Struct({
18578
+ head: Struct({ sha: String4 }),
18579
+ merged_at: NullOr(String4)
18580
+ })));
18581
+ var StackedPullRequest = Struct({
18582
+ number: PullRequestNumberSchema,
18583
+ state: Literals(["closed", "open"])
18584
+ });
18585
+ var Stacks = responseOf()(ArraySchema(Struct({ number: StackNumberSchema, pull_requests: ArraySchema(StackedPullRequest) })));
18586
+ var RepositoryDetails = responseOf()(Struct({ default_branch: String4 }));
18587
+ var MergeRequest = Struct({
18588
+ details: Struct({
18589
+ message: String4,
18590
+ uuid: optionalKey2(String4)
18591
+ }),
18592
+ status: Literals(["enqueued", "failed", "merged", "pending"])
18593
+ });
18594
+ var StartedMerge = responseOf()(MergeRequest);
18595
+ var MergeResult = responseOf()(MergeRequest);
18596
+ var GitHubLive = (repository, connection) => {
18597
+ const repo = (path) => `/repos/${repository.owner}/${repository.name}${path}`;
18598
+ return effect(GitHub, gen2(function* () {
18599
+ const requestInit = match(connection.proxy, {
18600
+ onNone: () => ({}),
18601
+ onSome: ({ ca, url }) => match(ca, {
18602
+ onNone: () => ({ proxy: url }),
18603
+ onSome: (certificates) => ({ proxy: url, tls: { ca: certificates } })
18604
+ })
18605
+ });
18606
+ const client = (yield* HttpClient).pipe(mapRequest((request) => request.pipe(prependUrl("https://api.github.com"), acceptJson, bearerToken(connection.token), setHeaders({
18607
+ "user-agent": "jj-octo",
18608
+ "x-github-api-version": "2022-11-28"
18609
+ }))));
18610
+ const send = (request) => client.execute(request).pipe(provideService2(RequestInit, requestInit), flatMap4((response) => response.status < 400 ? succeed6(response) : response.text.pipe(orElseSucceed2(() => ""), flatMap4((text) => fail6(new GitHubFailed({
18611
+ message: `${request.method} ${request.url} returned ${response.status}: ${text}`
18612
+ }))))), catchTag2("HttpClientError", (error) => fail6(new GitHubFailed({ message: error.message }))));
18613
+ const json = (schema) => (request) => send(request).pipe(flatMap4(schemaBodyJson(schema)), catchTags2({
18614
+ HttpClientError: (error) => fail6(new GitHubFailed({ message: error.message })),
18615
+ SchemaError: (error) => fail6(new GitHubFailed({
18616
+ message: `unexpected response to ${request.url}: ${error.message}`
18617
+ }))
18618
+ }));
18619
+ const stackOf = (pullRequest) => json(Stacks)(get4(repo(`/stacks?pull_request=${pullRequest}`))).pipe(map7((stacks) => map(fromNullishOr(stacks[0]), (stack) => ({
18620
+ number: stack.number,
18621
+ open: stack.pull_requests.filter(({ state }) => state === "open").map(({ number }) => number)
18622
+ }))));
18623
+ const mergeRequest = (path) => json(MergeResult)(get4(repo(path)));
18624
+ const fromBranch = (branch, state) => repo(`/pulls?state=${state}&head=${repository.owner}:${encodeURIComponent(branch)}`);
18625
+ return {
18626
+ addToStack: (stack, pullRequests) => send(post(repo(`/stacks/${stack}/add`)).pipe(bodyOf({ pull_requests: [...pullRequests] }))).pipe(asVoid2),
18627
+ createPullRequest: ({ base, body, draft, head, title }) => json(CreatedPullRequest)(post(repo("/pulls")).pipe(bodyOf({ base, body, draft, head, title }))).pipe(map7(({ number }) => number)),
18628
+ createStack: (pullRequests) => json(CreatedStack)(post(repo("/stacks")).pipe(bodyOf({ pull_requests: [...pullRequests] }))).pipe(map7(({ number }) => number)),
18629
+ defaultBranch: json(RepositoryDetails)(get4(repo(""))).pipe(map7(({ default_branch }) => default_branch)),
18630
+ mergePullRequest: (pullRequest) => send(put(repo(`/pulls/${pullRequest}/merge`)).pipe(bodyOf({ merge_method: "squash" }))).pipe(asVoid2),
18631
+ mergeStack: (pullRequest) => gen2(function* () {
18632
+ const started = yield* json(StartedMerge)(put(repo(`/pulls/${pullRequest}/merge-async`)).pipe(bodyOf({
18633
+ merge_action: "default",
18634
+ merge_method: "squash"
18635
+ })));
18636
+ const { uuid } = started.details;
18637
+ if (uuid === undefined) {
18638
+ return yield* new GitHubFailed({
18639
+ message: `merging #${pullRequest} returned no request to follow`
18640
+ });
18641
+ }
18642
+ const settled = yield* mergeRequest(`/pulls/${pullRequest}/merge-async/${uuid}`).pipe(repeat2({
18643
+ schedule: spaced("5 seconds"),
18644
+ until: ({ status }) => status !== "pending"
18645
+ }));
18646
+ if (settled.status === "merged" || settled.status === "enqueued") {
18647
+ return settled.status;
18648
+ }
18649
+ return yield* new GitHubFailed({
18650
+ message: `merging #${pullRequest} ended ${settled.status}: ${settled.details.message}`
18651
+ });
18652
+ }),
18653
+ mergedHeads: (branch) => json(ClosedPullRequests)(get4(fromBranch(branch, "closed"))).pipe(map7((closed) => closed.filter(({ merged_at }) => merged_at !== null).map(({ head }) => head.sha))),
18654
+ openPullRequest: (branch) => json(PullRequests)(get4(fromBranch(branch, "open"))).pipe(map7((found) => map(fromNullishOr(found[0]), ({ base, body, draft, number, title }) => ({
18655
+ base: base.ref,
18656
+ body: body ?? "",
18657
+ draft: draft ?? false,
18658
+ number,
18659
+ title
18660
+ })))),
18661
+ setBase: (pullRequest, base) => send(patch(repo(`/pulls/${pullRequest}`)).pipe(bodyOf({ base }))).pipe(asVoid2),
18662
+ setDescription: (pullRequest, { body, title }) => send(patch(repo(`/pulls/${pullRequest}`)).pipe(bodyOf({ body, title }))).pipe(asVoid2),
18663
+ stackOf,
18664
+ unstack: (stack) => send(post(repo(`/stacks/${stack}/unstack`))).pipe(asVoid2)
18665
+ };
18666
+ }));
18667
+ };
18668
+
18547
18669
  // src/cli/gh/stack/trunk.ts
18548
18670
  class TrunkMismatch extends TaggedError2("TrunkMismatch") {
18549
18671
  }
@@ -18555,6 +18677,11 @@ var checkTrunk = (branch) => gen2(function* () {
18555
18677
  });
18556
18678
  }
18557
18679
  });
18680
+ var checkedTrunk = gen2(function* () {
18681
+ const branch = yield* (yield* GitHub).defaultBranch;
18682
+ yield* checkTrunk(branch);
18683
+ return branch;
18684
+ });
18558
18685
 
18559
18686
  // src/cli/gh/setup.ts
18560
18687
  class NotSetUp extends TaggedError2("NotSetUp") {
@@ -18688,121 +18815,6 @@ var init = gen2(function* () {
18688
18815
  });
18689
18816
  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."));
18690
18817
 
18691
- // src/clients/github/openapi.ts
18692
- var responseOf = () => (schema) => schema;
18693
- var bodyOf = (body) => bodyJsonUnsafe(body);
18694
-
18695
- // src/clients/github/types.ts
18696
- var PullRequestNumberSchema = Number6.pipe(brand("PullRequestNumber"));
18697
- var StackNumberSchema = Number6.pipe(brand("StackNumber"));
18698
-
18699
- // src/clients/github/client.ts
18700
- class GitHubFailed extends TaggedError2("GitHubFailed") {
18701
- }
18702
-
18703
- class GitHub extends Service()("jj-octo/GitHub") {
18704
- }
18705
- var CreatedPullRequest = responseOf()(Struct({ number: PullRequestNumberSchema }));
18706
- var CreatedStack = responseOf()(Struct({ number: StackNumberSchema }));
18707
- var PullRequests = responseOf()(ArraySchema(Struct({
18708
- base: Struct({ ref: String4 }),
18709
- body: NullOr(String4),
18710
- draft: optionalKey2(Boolean3),
18711
- number: PullRequestNumberSchema,
18712
- title: String4
18713
- })));
18714
- var ClosedPullRequests = responseOf()(ArraySchema(Struct({
18715
- head: Struct({ sha: String4 }),
18716
- merged_at: NullOr(String4)
18717
- })));
18718
- var StackedPullRequest = Struct({
18719
- number: PullRequestNumberSchema,
18720
- state: Literals(["closed", "open"])
18721
- });
18722
- var Stacks = responseOf()(ArraySchema(Struct({ number: StackNumberSchema, pull_requests: ArraySchema(StackedPullRequest) })));
18723
- var RepositoryDetails = responseOf()(Struct({ default_branch: String4 }));
18724
- var MergeRequest = Struct({
18725
- details: Struct({
18726
- message: String4,
18727
- uuid: optionalKey2(String4)
18728
- }),
18729
- status: Literals(["enqueued", "failed", "merged", "pending"])
18730
- });
18731
- var StartedMerge = responseOf()(MergeRequest);
18732
- var MergeResult = responseOf()(MergeRequest);
18733
- var GitHubLive = (repository, connection) => {
18734
- const repo = (path) => `/repos/${repository.owner}/${repository.name}${path}`;
18735
- return effect(GitHub, gen2(function* () {
18736
- const requestInit = match(connection.proxy, {
18737
- onNone: () => ({}),
18738
- onSome: ({ ca, url }) => match(ca, {
18739
- onNone: () => ({ proxy: url }),
18740
- onSome: (certificates) => ({ proxy: url, tls: { ca: certificates } })
18741
- })
18742
- });
18743
- const client = (yield* HttpClient).pipe(mapRequest((request) => request.pipe(prependUrl("https://api.github.com"), acceptJson, bearerToken(connection.token), setHeaders({
18744
- "user-agent": "jj-octo",
18745
- "x-github-api-version": "2022-11-28"
18746
- }))));
18747
- const send = (request) => client.execute(request).pipe(provideService2(RequestInit, requestInit), flatMap4((response) => response.status < 400 ? succeed6(response) : response.text.pipe(orElseSucceed2(() => ""), flatMap4((text) => fail6(new GitHubFailed({
18748
- message: `${request.method} ${request.url} returned ${response.status}: ${text}`
18749
- }))))), catchTag2("HttpClientError", (error) => fail6(new GitHubFailed({ message: error.message }))));
18750
- const json = (schema) => (request) => send(request).pipe(flatMap4(schemaBodyJson(schema)), catchTags2({
18751
- HttpClientError: (error) => fail6(new GitHubFailed({ message: error.message })),
18752
- SchemaError: (error) => fail6(new GitHubFailed({
18753
- message: `unexpected response to ${request.url}: ${error.message}`
18754
- }))
18755
- }));
18756
- const stackOf = (pullRequest) => json(Stacks)(get4(repo(`/stacks?pull_request=${pullRequest}`))).pipe(map7((stacks) => map(fromNullishOr(stacks[0]), (stack) => ({
18757
- number: stack.number,
18758
- open: stack.pull_requests.filter(({ state }) => state === "open").map(({ number }) => number)
18759
- }))));
18760
- const mergeRequest = (path) => json(MergeResult)(get4(repo(path)));
18761
- const fromBranch = (branch, state) => repo(`/pulls?state=${state}&head=${repository.owner}:${encodeURIComponent(branch)}`);
18762
- return {
18763
- addToStack: (stack, pullRequests) => send(post(repo(`/stacks/${stack}/add`)).pipe(bodyOf({ pull_requests: [...pullRequests] }))).pipe(asVoid2),
18764
- createPullRequest: ({ base, body, draft, head, title }) => json(CreatedPullRequest)(post(repo("/pulls")).pipe(bodyOf({ base, body, draft, head, title }))).pipe(map7(({ number }) => number)),
18765
- createStack: (pullRequests) => json(CreatedStack)(post(repo("/stacks")).pipe(bodyOf({ pull_requests: [...pullRequests] }))).pipe(map7(({ number }) => number)),
18766
- defaultBranch: json(RepositoryDetails)(get4(repo(""))).pipe(map7(({ default_branch }) => default_branch)),
18767
- mergePullRequest: (pullRequest) => send(put(repo(`/pulls/${pullRequest}/merge`)).pipe(bodyOf({ merge_method: "squash" }))).pipe(asVoid2),
18768
- mergeStack: (pullRequest) => gen2(function* () {
18769
- const started = yield* json(StartedMerge)(put(repo(`/pulls/${pullRequest}/merge-async`)).pipe(bodyOf({
18770
- merge_action: "default",
18771
- merge_method: "squash"
18772
- })));
18773
- const { uuid } = started.details;
18774
- if (uuid === undefined) {
18775
- return yield* new GitHubFailed({
18776
- message: `merging #${pullRequest} returned no request to follow`
18777
- });
18778
- }
18779
- const settled = yield* mergeRequest(`/pulls/${pullRequest}/merge-async/${uuid}`).pipe(repeat2({
18780
- schedule: spaced("5 seconds"),
18781
- until: ({ status }) => status !== "pending"
18782
- }));
18783
- if (settled.status === "merged" || settled.status === "enqueued") {
18784
- return settled.status;
18785
- }
18786
- return yield* new GitHubFailed({
18787
- message: `merging #${pullRequest} ended ${settled.status}: ${settled.details.message}`
18788
- });
18789
- }),
18790
- mergedHeads: (branch) => json(ClosedPullRequests)(get4(fromBranch(branch, "closed"))).pipe(map7((closed) => closed.filter(({ merged_at }) => merged_at !== null).map(({ head }) => head.sha))),
18791
- openPullRequest: (branch) => json(PullRequests)(get4(fromBranch(branch, "open"))).pipe(map7((found) => map(fromNullishOr(found[0]), ({ base, body, draft, number, title }) => ({
18792
- base: base.ref,
18793
- body: body ?? "",
18794
- draft: draft ?? false,
18795
- number,
18796
- title
18797
- })))),
18798
- setBase: (pullRequest, base) => send(patch(repo(`/pulls/${pullRequest}`)).pipe(bodyOf({ base }))).pipe(asVoid2),
18799
- setDescription: (pullRequest, { body, title }) => send(patch(repo(`/pulls/${pullRequest}`)).pipe(bodyOf({ body, title }))).pipe(asVoid2),
18800
- stackOf,
18801
- unstack: (stack) => send(post(repo(`/stacks/${stack}/unstack`))).pipe(asVoid2)
18802
- };
18803
- }));
18804
- };
18805
-
18806
18818
  // src/clients/github/connection.ts
18807
18819
  var authKey = "jj-octo.github-auth";
18808
18820
  var ownToken = gen2(function* () {
@@ -19003,115 +19015,24 @@ var syncDescription = ({ bookmark, description }, pullRequest, { ignoreClaudeCod
19003
19015
  })), exhaustive2);
19004
19016
  });
19005
19017
 
19006
- // src/cli/gh/stack/publish.ts
19007
- class SeveralStacks extends TaggedError2("SeveralStacks") {
19008
- }
19009
- var stackContaining = (pullRequests) => gen2(function* () {
19010
- const github = yield* GitHub;
19011
- const found = yield* all2(pullRequests.map((pullRequest) => github.stackOf(pullRequest)), { concurrency: "unbounded" });
19012
- const stacks = dedupeWith(found.flatMap((stack) => toArray(stack)), (a, b) => a.number === b.number);
19013
- if (stacks.length > 1) {
19014
- return yield* new SeveralStacks({
19015
- message: `the pull requests belong to stacks ${stacks.map(({ number }) => `#${number}`).join(" and ")}; unstack all but one`
19016
- });
19017
- }
19018
- return fromNullishOr(stacks[0]);
19019
- });
19020
- var register = (remaining, pullRequests) => gen2(function* () {
19021
- const github = yield* GitHub;
19022
- 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);
19023
- });
19024
- var publish = (stack, { draft, overwriteDescriptions }) => gen2(function* () {
19025
- const github = yield* GitHub;
19026
- const jj = yield* Jj;
19027
- const bookmarks = stack.map(({ bookmark }) => bookmark);
19028
- const trunk = yield* github.defaultBranch;
19029
- const bases = basesFor(bookmarks, trunk);
19030
- const found = yield* all2(stack.map((change, index) => github.openPullRequest(change.bookmark).pipe(map7((pullRequest) => ({ base: bases[index] ?? trunk, change, pullRequest })))), { concurrency: "unbounded" });
19031
- const targets = yield* bookmarkTargets;
19032
- const pushedDescriptions = yield* descriptionsOf(bookmarks.flatMap((bookmark) => targets.get(bookmark)?.origin ?? []));
19033
- const ignoreClaudeCodeFooter = yield* ignoresClaudeCodeFooter;
19034
- const changes = [];
19035
- for (const entry of found) {
19036
- const pushed = targets.get(entry.change.bookmark)?.origin;
19037
- changes.push({
19038
- ...entry,
19039
- description: isNone2(entry.pullRequest) ? none2() : yield* syncDescription(entry.change, entry.pullRequest.value, {
19040
- ignoreClaudeCodeFooter,
19041
- overwriteDescriptions,
19042
- pushedDescription: (pushed === undefined ? undefined : pushedDescriptions.get(pushed)) ?? ""
19043
- })
19044
- });
19045
- }
19046
- const moving = changes.flatMap(({ base, pullRequest }) => isSome2(pullRequest) && pullRequest.value.base !== base ? [pullRequest.value] : []);
19047
- const wanted = changes.map(({ pullRequest }) => isSome2(pullRequest) ? some2(pullRequest.value.number) : none2());
19048
- const existing = yield* stackContaining(wanted.flatMap((number) => toArray(number)));
19049
- const dissolve = mustDissolve(existing, wanted, moving.length > 0);
19050
- if (dissolve && isSome2(existing)) {
19051
- yield* github.unstack(existing.value.number);
19052
- yield* error(`dissolved stack #${existing.value.number}`);
19053
- }
19054
- for (const { number } of moving.filter(({ base }) => base !== trunk)) {
19055
- yield* github.setBase(number, trunk);
19056
- }
19057
- const moved = new Set(moving.map(({ number }) => number));
19058
- for (const bookmark of bookmarks.filter((name) => targets.get(name)?.tracked !== true)) {
19059
- yield* jj.run(["bookmark", "track", `exact:${bookmark}`, "--remote", "origin"]);
19060
- }
19061
- yield* jj.run([
19062
- "git",
19063
- "push",
19064
- ...bookmarks.flatMap((bookmark) => ["--bookmark", `exact:${bookmark}`])
19065
- ]);
19066
- const numbers = [];
19067
- for (const { base, change, description, pullRequest } of changes) {
19068
- if (isNone2(pullRequest)) {
19069
- const created = yield* github.createPullRequest({
19070
- ...titleAndBody(change.description),
19071
- base,
19072
- draft,
19073
- head: change.bookmark
19074
- });
19075
- yield* error(`opened #${created} for ${change.bookmark}`);
19076
- numbers.push(created);
19077
- } else {
19078
- const { number } = pullRequest.value;
19079
- if (moved.has(number) && base !== trunk) {
19080
- yield* github.setBase(number, base);
19081
- }
19082
- if (isSome2(description)) {
19083
- yield* github.setDescription(number, titleAndBody(description.value));
19084
- yield* error(`updated the description of #${number}`);
19085
- }
19086
- numbers.push(number);
19087
- }
19088
- }
19089
- yield* register(dissolve ? none2() : existing, numbers);
19090
- });
19091
- var submit = (revision, options) => gen2(function* () {
19092
- yield* checkTrunk(yield* (yield* GitHub).defaultBranch);
19093
- yield* publish(yield* downstackChanges(revision), options);
19094
- });
19095
- var republish = (revision) => gen2(function* () {
19096
- if ((yield* changesIn(stackRevset(revision))).length === 0) {
19097
- return;
19098
- }
19099
- const stack = yield* stackChanges(revision);
19100
- const targets = yield* bookmarkTargets;
19101
- const top = stack.findLastIndex(({ bookmark }) => targets.get(bookmark)?.origin !== undefined);
19102
- if (top !== -1) {
19103
- yield* publish(stack.slice(0, top + 1), { draft: true, overwriteDescriptions: false });
19104
- }
19105
- });
19106
-
19107
- // src/cli/gh/sync.ts
19108
- var BEHIND_ROOTS = `(${STACK_ROOTS}) ~ children(trunk())`;
19109
- var bookmarksBeforeFetch = gen2(function* () {
19018
+ // src/cli/gh/stack/combine.ts
19019
+ var pushedOf = (bookmarks) => bookmarks.filter(({ remote }) => remote !== undefined);
19020
+ var bookmarksBeforeFetch = (revset) => gen2(function* () {
19110
19021
  const targets = [...yield* bookmarkTargets];
19111
- const bottomFirst = lines2(yield* readLog("mutable() & bookmarks()", String.raw`commit_id ++ "\t" ++ change_id ++ "\n"`));
19022
+ const bookmarked = `(${revset}) & bookmarks()`;
19023
+ const bottomFirst = lines2(yield* readLog(bookmarked, String.raw`commit_id ++ "\t" ++ change_id ++ "\t" ++ current_working_copy ++ "\n"`));
19024
+ 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"));
19112
19025
  return bottomFirst.toReversed().flatMap((line) => {
19113
- const [commit = "", change = ""] = line.split("\t");
19114
- return targets.filter(([, { local }]) => local === commit).map(([name, { origin }]) => ({ change, local: commit, name, remote: origin }));
19026
+ const [commit = "", change = "", workingCopy] = line.split("\t");
19027
+ const above = children.flatMap(([parents = "", child = ""]) => parents.split(",").includes(commit) ? [child] : []);
19028
+ return targets.filter(([, { local }]) => local === commit).map(([name, { origin }]) => ({
19029
+ above,
19030
+ change,
19031
+ editing: workingCopy === "true",
19032
+ local: commit,
19033
+ name,
19034
+ remote: origin
19035
+ }));
19115
19036
  });
19116
19037
  });
19117
19038
  var localCommit = (change, remote, fetched) => gen2(function* () {
@@ -19122,22 +19043,31 @@ var localCommit = (change, remote, fetched) => gen2(function* () {
19122
19043
  }
19123
19044
  return local;
19124
19045
  });
19125
- var combine4 = ({ change, local: localBefore, name, remote }, fetched) => gen2(function* () {
19046
+ var combine4 = ({ above, change, editing, local: localBefore, name, remote }, fetched) => gen2(function* () {
19126
19047
  const jj = yield* Jj;
19127
19048
  if (remote === undefined || fetched === undefined || fetched === remote) {
19128
19049
  return true;
19129
19050
  }
19130
19051
  const [local, ...others] = yield* localCommit(change, remote, fetched);
19131
19052
  if (local === undefined) {
19053
+ if (editing) {
19054
+ yield* jj.run(["edit", fetched]);
19055
+ }
19056
+ const moved = `(${anyOf(above.map((child) => `change_id(${child})`))}) ~ children(${fetched})`;
19057
+ if ((yield* commitsAt(moved)).length > 0) {
19058
+ yield* jj.run(["rebase", "--source", moved, "--destination", fetched]);
19059
+ yield* error(`took GitHub's rewritten ${name}, and moved what was on it there`);
19060
+ }
19132
19061
  return true;
19133
19062
  }
19134
19063
  if (others.length > 0) {
19135
19064
  return false;
19136
19065
  }
19137
19066
  const keepLocal = jj.run(["bookmark", "set", name, "-r", local, "--allow-backwards"]);
19067
+ const held = `::(bookmarks() ~ ${bookmarkRevset(name)}) ~ ::${remote}`;
19138
19068
  const dropCopies = jj.run([
19139
19069
  "abandon",
19140
- `((::${fetched} | ::${remote}) & mutable()) ~ ::${bookmarkRevset(name)}`
19070
+ `((::${fetched} | ::${remote}) & mutable()) ~ ::${bookmarkRevset(name)} ~ (${held})`
19141
19071
  ]);
19142
19072
  if ((yield* commitsAt(`${remote} & ::${fetched}`)).length > 0) {
19143
19073
  yield* keepLocal;
@@ -19155,6 +19085,9 @@ var combine4 = ({ change, local: localBefore, name, remote }, fetched) => gen2(f
19155
19085
  }
19156
19086
  if (localBefore === remote) {
19157
19087
  yield* jj.run(["rebase", "--source", `children(${local})`, "--destination", fetched]);
19088
+ if (editing) {
19089
+ yield* jj.run(["edit", fetched]);
19090
+ }
19158
19091
  yield* jj.run(["abandon", local]);
19159
19092
  yield* error(`took GitHub's rewritten ${name}, which wasn't changed locally`);
19160
19093
  return true;
@@ -19169,6 +19102,9 @@ var combine4 = ({ change, local: localBefore, name, remote }, fetched) => gen2(f
19169
19102
  return false;
19170
19103
  });
19171
19104
 
19105
+ // src/cli/gh/stack/reconcile.ts
19106
+ var BEHIND_ROOTS = `(${STACK_ROOTS}) ~ children(trunk())`;
19107
+
19172
19108
  class NotCombined extends TaggedError2("NotCombined") {
19173
19109
  }
19174
19110
  var rebaseStack = (root, required) => gen2(function* () {
@@ -19188,8 +19124,8 @@ var rebaseStack = (root, required) => gen2(function* () {
19188
19124
  String.raw`id ++ "\n"`
19189
19125
  ]));
19190
19126
  yield* jj.run(["rebase", "--source", root, "--destination", "trunk()", "--skip-emptied"]);
19191
- const rebased = ["none()", ...changes.map((change) => `change_id(${change})`)].join(" | ");
19192
- const untouched = ["none()", ...conflictedCommits].join(" | ");
19127
+ const rebased = anyOf(changes.map((change) => `change_id(${change})`));
19128
+ const untouched = anyOf(conflictedCommits);
19193
19129
  const conflictedNow = yield* changeIdsAt(`(conflicts() & (${rebased})) ~ (${untouched})`);
19194
19130
  const mayRestore = !changes.some((change) => required.includes(change));
19195
19131
  if (mayRestore && conflictedNow.some((change) => !conflicted.includes(change))) {
@@ -19197,23 +19133,51 @@ var rebaseStack = (root, required) => gen2(function* () {
19197
19133
  yield* error(`left "${name}" and the changes on it where they were: they conflict with the trunk`);
19198
19134
  }
19199
19135
  });
19200
- var mergedOnGitHub = (bookmarks) => gen2(function* () {
19136
+ var notMerged = (merged) => (name) => merged.every(({ bookmark }) => bookmark !== name);
19137
+ var mergedHeadsOf = (bookmarks) => gen2(function* () {
19201
19138
  const github = yield* GitHub;
19202
- 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" });
19203
- return merged.flat();
19139
+ const pushed = pushedOf(bookmarks);
19140
+ const heads = yield* forEach2(pushed, ({ name }) => github.mergedHeads(name), {
19141
+ concurrency: "unbounded"
19142
+ });
19143
+ return Object.fromEntries(pushed.map(({ name }, index) => [name, heads[index] ?? []]));
19204
19144
  });
19205
- var sync4 = (known = []) => gen2(function* () {
19145
+ var mergedIn = (bookmarks, heads) => gen2(function* () {
19206
19146
  const jj = yield* Jj;
19207
- const before = yield* bookmarksBeforeFetch;
19208
- const merged = [
19209
- ...known,
19210
- ...yield* mergedOnGitHub(before.filter(({ name }) => known.every(({ bookmark }) => bookmark !== name)))
19211
- ];
19212
- yield* jj.run(["git", "fetch"]);
19213
- for (const { bookmark } of merged) {
19214
- yield* jj.run(["bookmark", "forget", "--include-remotes", `exact:${bookmark}`]);
19147
+ const merged = yield* forEach2(bookmarks, ({ change, local, name, remote }) => gen2(function* () {
19148
+ const merges = heads[name] ?? [];
19149
+ const rebasedOnly = remote !== undefined && merges.includes(remote) && (yield* jj.read(["interdiff", "--from", remote, "--to", local, "--summary"])).trim() === "";
19150
+ return merges.includes(local) || rebasedOnly ? [{ bookmark: name, changeId: change }] : [];
19151
+ }));
19152
+ return merged.flat();
19153
+ });
19154
+ var mergedOnGitHub = (bookmarks) => flatMap4(mergedHeadsOf(bookmarks), (heads) => mergedIn(bookmarks, heads));
19155
+ var forget = (names) => gen2(function* () {
19156
+ const listed = yield* bookmarkTargets;
19157
+ const present = names.filter((name) => listed.has(name));
19158
+ if (present.length > 0) {
19159
+ const jj = yield* Jj;
19160
+ yield* jj.run([
19161
+ "bookmark",
19162
+ "forget",
19163
+ "--include-remotes",
19164
+ ...present.map((name) => `exact:${name}`)
19165
+ ]);
19215
19166
  }
19216
- const mergedRevset = `(${["none()", ...merged.map(({ changeId }) => `change_id(${changeId})`)].join(" | ")}) & mutable()`;
19167
+ });
19168
+ var reconcile = ({
19169
+ before,
19170
+ merged,
19171
+ scope
19172
+ }) => gen2(function* () {
19173
+ const jj = yield* Jj;
19174
+ const branches = scope === "every stack" ? [] : [scope.trunk, ...pushedOf(before).map(({ name }) => name)].flatMap((branch) => [
19175
+ "--branch",
19176
+ `exact:${branch}`
19177
+ ]);
19178
+ yield* jj.run(["git", "fetch", ...branches]);
19179
+ yield* forget(merged.map(({ bookmark }) => bookmark));
19180
+ const mergedRevset = `(${anyOf(merged.map(({ changeId }) => `change_id(${changeId})`))}) & mutable()`;
19217
19181
  const required = yield* changeIdsAt(`(${mergedRevset}):: ~ (${mergedRevset})`);
19218
19182
  if (merged.length > 0) {
19219
19183
  yield* jj.run(["abandon", mergedRevset]);
@@ -19221,18 +19185,20 @@ var sync4 = (known = []) => gen2(function* () {
19221
19185
  }
19222
19186
  const fetched = yield* bookmarkTargets;
19223
19187
  const notCombined = [];
19224
- for (const bookmark of before.filter(({ name }) => merged.every((change) => change.bookmark !== name))) {
19188
+ for (const bookmark of before.filter(({ name }) => notMerged(merged)(name))) {
19225
19189
  if (!(yield* combine4(bookmark, fetched.get(bookmark.name)?.origin))) {
19226
19190
  notCombined.push(bookmark.name);
19227
19191
  }
19228
19192
  }
19229
- for (const root of yield* commitsAt(BEHIND_ROOTS)) {
19193
+ const roots = scope === "every stack" ? BEHIND_ROOTS : `(${BEHIND_ROOTS}) & ::(${anyOf([
19194
+ ...scope.bookmarks.map((name) => bookmarkRevset(name)),
19195
+ ...scope.unnamed.map((change) => `change_id(${change})`)
19196
+ ])})`;
19197
+ for (const root of yield* commitsAt(roots)) {
19230
19198
  yield* rebaseStack(root, required);
19231
19199
  }
19232
- const onTrunk = (yield* bookmarksAt("::trunk()")).filter((name) => before.some((bookmark) => bookmark.name === name));
19233
- for (const name of onTrunk) {
19234
- yield* jj.run(["bookmark", "forget", "--include-remotes", `exact:${name}`]);
19235
- }
19200
+ const onStacks = new Set(scope === "every stack" ? before.map(({ name }) => name) : scope.bookmarks);
19201
+ yield* forget((yield* bookmarksAt("::trunk()")).filter((name) => onStacks.has(name)));
19236
19202
  if (notCombined.length > 0) {
19237
19203
  yield* new NotCombined({
19238
19204
  message: [
@@ -19244,8 +19210,201 @@ var sync4 = (known = []) => gen2(function* () {
19244
19210
  });
19245
19211
  }
19246
19212
  });
19213
+
19214
+ // src/cli/gh/stack/sync-stack.ts
19215
+ class BranchesDeleted extends TaggedError2("BranchesDeleted") {
19216
+ }
19217
+ var whyDeleted = (heads, { remote }) => {
19218
+ if (remote !== undefined && heads.includes(remote)) {
19219
+ return "its pushed commit merged, and the change was edited since";
19220
+ }
19221
+ return heads.length > 0 ? "a pull request from it merged, but not the commit pushed for this change" : "no pull request from it merged";
19222
+ };
19223
+ var changedOnGitHub = (bookmarks) => gen2(function* () {
19224
+ const pushed = pushedOf(bookmarks);
19225
+ if (pushed.length === 0) {
19226
+ return { changed: [], deleted: [] };
19227
+ }
19228
+ const git = yield* Git;
19229
+ const gitDirectory = (yield* (yield* Jj).read(["git", "root"])).trim();
19230
+ const output = yield* git.read([
19231
+ "--git-dir",
19232
+ gitDirectory,
19233
+ "ls-remote",
19234
+ "origin",
19235
+ ...pushed.map(({ name }) => `refs/heads/${name}`)
19236
+ ]);
19237
+ const onGitHub = new Map(lines2(output).map((line) => {
19238
+ const [commit = "", ref = ""] = line.split("\t");
19239
+ return [ref.replace(/^refs\/heads\//u, ""), commit];
19240
+ }));
19241
+ return {
19242
+ changed: pushed.filter(({ name, remote }) => onGitHub.has(name) && onGitHub.get(name) !== remote).map(({ name }) => name),
19243
+ deleted: pushed.filter(({ name }) => !onGitHub.has(name))
19244
+ };
19245
+ });
19246
+ var syncStack = (revision, trunk) => gen2(function* () {
19247
+ const downstack = downstackRevset(revision);
19248
+ const before = yield* bookmarksBeforeFetch(downstack);
19249
+ const [heads, onGitHub] = yield* all2([mergedHeadsOf(before), changedOnGitHub(before)], {
19250
+ concurrency: "unbounded"
19251
+ });
19252
+ const merged = yield* mergedIn(before, heads);
19253
+ const changed = onGitHub.changed.filter(notMerged(merged));
19254
+ const deleted = onGitHub.deleted.filter(({ name }) => notMerged(merged)(name));
19255
+ if (deleted.length > 0) {
19256
+ return yield* new BranchesDeleted({
19257
+ message: [
19258
+ "these branches were deleted on GitHub, so nothing was changed:",
19259
+ ...deleted.map((bookmark) => `${bookmark.name}: ${whyDeleted(heads[bookmark.name] ?? [], bookmark)}`),
19260
+ "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>`"
19261
+ ].join(`
19262
+ `)
19263
+ });
19264
+ }
19265
+ const found = [
19266
+ ...merged.map(({ bookmark }) => `${bookmark} merged`),
19267
+ ...changed.map((name) => `${name} changed`)
19268
+ ];
19269
+ if (found.length === 0) {
19270
+ return false;
19271
+ }
19272
+ yield* error(`on GitHub, ${found.join(", ")}; syncing the stack first`);
19273
+ const whole = stackRevset(revision);
19274
+ yield* reconcile({
19275
+ before,
19276
+ merged,
19277
+ scope: {
19278
+ bookmarks: yield* bookmarksAt(whole),
19279
+ trunk,
19280
+ unnamed: yield* changeIdsAt(`((${whole}) | (@ & children(${whole}))) ~ bookmarks()`)
19281
+ }
19282
+ });
19283
+ return true;
19284
+ });
19285
+
19286
+ // src/cli/gh/stack/publish.ts
19287
+ class SeveralStacks extends TaggedError2("SeveralStacks") {
19288
+ }
19289
+ var stackContaining = (pullRequests) => gen2(function* () {
19290
+ const github = yield* GitHub;
19291
+ const found = yield* all2(pullRequests.map((pullRequest) => github.stackOf(pullRequest)), { concurrency: "unbounded" });
19292
+ const stacks = dedupeWith(found.flatMap((stack) => toArray(stack)), (a, b) => a.number === b.number);
19293
+ if (stacks.length > 1) {
19294
+ return yield* new SeveralStacks({
19295
+ message: `the pull requests belong to stacks ${stacks.map(({ number }) => `#${number}`).join(" and ")}; unstack all but one`
19296
+ });
19297
+ }
19298
+ return fromNullishOr(stacks[0]);
19299
+ });
19300
+ var register = (remaining, pullRequests) => gen2(function* () {
19301
+ const github = yield* GitHub;
19302
+ 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);
19303
+ });
19304
+ var publish = (stack, { draft, overwriteDescriptions, trunk }) => gen2(function* () {
19305
+ const github = yield* GitHub;
19306
+ const jj = yield* Jj;
19307
+ const bookmarks = stack.map(({ bookmark }) => bookmark);
19308
+ const bases = basesFor(bookmarks, trunk);
19309
+ const found = yield* all2(stack.map((change, index) => github.openPullRequest(change.bookmark).pipe(map7((pullRequest) => ({ base: bases[index] ?? trunk, change, pullRequest })))), { concurrency: "unbounded" });
19310
+ const targets = yield* bookmarkTargets;
19311
+ const pushedDescriptions = yield* descriptionsOf(bookmarks.flatMap((bookmark) => targets.get(bookmark)?.origin ?? []));
19312
+ const ignoreClaudeCodeFooter = yield* ignoresClaudeCodeFooter;
19313
+ const changes = [];
19314
+ for (const entry of found) {
19315
+ const pushed = targets.get(entry.change.bookmark)?.origin;
19316
+ changes.push({
19317
+ ...entry,
19318
+ description: isNone2(entry.pullRequest) ? none2() : yield* syncDescription(entry.change, entry.pullRequest.value, {
19319
+ ignoreClaudeCodeFooter,
19320
+ overwriteDescriptions,
19321
+ pushedDescription: (pushed === undefined ? undefined : pushedDescriptions.get(pushed)) ?? ""
19322
+ })
19323
+ });
19324
+ }
19325
+ const moving = changes.flatMap(({ base, pullRequest }) => isSome2(pullRequest) && pullRequest.value.base !== base ? [pullRequest.value] : []);
19326
+ const wanted = changes.map(({ pullRequest }) => isSome2(pullRequest) ? some2(pullRequest.value.number) : none2());
19327
+ const existing = yield* stackContaining(wanted.flatMap((number) => toArray(number)));
19328
+ const dissolve = mustDissolve(existing, wanted, moving.length > 0);
19329
+ if (dissolve && isSome2(existing)) {
19330
+ yield* github.unstack(existing.value.number);
19331
+ yield* error(`dissolved stack #${existing.value.number}`);
19332
+ }
19333
+ for (const { number } of moving.filter(({ base }) => base !== trunk)) {
19334
+ yield* github.setBase(number, trunk);
19335
+ }
19336
+ const moved = new Set(moving.map(({ number }) => number));
19337
+ for (const bookmark of bookmarks.filter((name) => targets.get(name)?.tracked !== true)) {
19338
+ yield* jj.run(["bookmark", "track", `exact:${bookmark}`, "--remote", "origin"]);
19339
+ }
19340
+ yield* jj.run([
19341
+ "git",
19342
+ "push",
19343
+ ...bookmarks.flatMap((bookmark) => ["--bookmark", `exact:${bookmark}`])
19344
+ ]);
19345
+ const numbers = [];
19346
+ for (const { base, change, description, pullRequest } of changes) {
19347
+ if (isNone2(pullRequest)) {
19348
+ const created = yield* github.createPullRequest({
19349
+ ...titleAndBody(change.description),
19350
+ base,
19351
+ draft,
19352
+ head: change.bookmark
19353
+ });
19354
+ yield* error(`opened #${created} for ${change.bookmark}`);
19355
+ numbers.push(created);
19356
+ } else {
19357
+ const { number } = pullRequest.value;
19358
+ if (moved.has(number) && base !== trunk) {
19359
+ yield* github.setBase(number, base);
19360
+ }
19361
+ if (isSome2(description)) {
19362
+ yield* github.setDescription(number, titleAndBody(description.value));
19363
+ yield* error(`updated the description of #${number}`);
19364
+ }
19365
+ numbers.push(number);
19366
+ }
19367
+ }
19368
+ yield* register(dissolve ? none2() : existing, numbers);
19369
+ });
19370
+ var submit = (revision, options) => gen2(function* () {
19371
+ const trunk = yield* checkedTrunk;
19372
+ const downstack = yield* downstackChanges(revision);
19373
+ if (!(yield* syncStack(revision, trunk))) {
19374
+ yield* publish(downstack, { ...options, trunk });
19375
+ return;
19376
+ }
19377
+ const left = yield* bookmarksAt(anyOf(downstack.map(({ bookmark }) => bookmarkRevset(bookmark))));
19378
+ const top = downstack.findLast(({ bookmark }) => left.includes(bookmark));
19379
+ if (top === undefined) {
19380
+ yield* error(`nothing left to submit: no bookmark of the stack is left after the sync`);
19381
+ return;
19382
+ }
19383
+ yield* publish(yield* downstackChanges(bookmarkRevset(top.bookmark)), { ...options, trunk });
19384
+ });
19385
+ var republish = (revision, trunk) => gen2(function* () {
19386
+ if ((yield* changesIn(stackRevset(revision))).length === 0) {
19387
+ return;
19388
+ }
19389
+ const stack = yield* stackChanges(revision);
19390
+ const targets = yield* bookmarkTargets;
19391
+ const top = stack.findLastIndex(({ bookmark }) => targets.get(bookmark)?.origin !== undefined);
19392
+ if (top !== -1) {
19393
+ yield* publish(stack.slice(0, top + 1), { draft: true, overwriteDescriptions: false, trunk });
19394
+ }
19395
+ });
19396
+
19397
+ // src/cli/gh/sync.ts
19398
+ var sync4 = (known = []) => gen2(function* () {
19399
+ const before = yield* bookmarksBeforeFetch("mutable()");
19400
+ const merged = [
19401
+ ...known,
19402
+ ...yield* mergedOnGitHub(before.filter(({ name }) => notMerged(known)(name)))
19403
+ ];
19404
+ yield* reconcile({ before, merged, scope: "every stack" });
19405
+ });
19247
19406
  var syncCommand = make31("sync", {}, () => asUserError(gen2(function* () {
19248
- yield* checkTrunk(yield* (yield* GitHub).defaultBranch);
19407
+ yield* checkedTrunk;
19249
19408
  yield* sync4();
19250
19409
  }).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."));
19251
19410
 
@@ -19281,8 +19440,7 @@ var mergeOnGitHub = ({ base, number }, trunk) => gen2(function* () {
19281
19440
  return "merged";
19282
19441
  });
19283
19442
  var merge5 = (revision) => gen2(function* () {
19284
- const trunk = yield* (yield* GitHub).defaultBranch;
19285
- yield* checkTrunk(trunk);
19443
+ const trunk = yield* checkedTrunk;
19286
19444
  const changes = yield* downstackChanges(revision);
19287
19445
  const top = changes.at(-1)?.bookmark ?? "";
19288
19446
  const targets = yield* bookmarkTargets;
@@ -19301,7 +19459,7 @@ var merge5 = (revision) => gen2(function* () {
19301
19459
  yield* sync4(changes);
19302
19460
  for (const bookmark of above) {
19303
19461
  if ((yield* commitsAt(bookmarkRevset(bookmark))).length > 0) {
19304
- yield* republish(bookmarkRevset(bookmark));
19462
+ yield* republish(bookmarkRevset(bookmark), trunk);
19305
19463
  break;
19306
19464
  }
19307
19465
  }
@@ -19314,7 +19472,7 @@ var submitCommand = make31("submit", {
19314
19472
  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")),
19315
19473
  publish: Boolean6("publish").pipe(withDefault5(false), withDescription2("Open new pull requests ready for review, instead of as drafts")),
19316
19474
  revision: revisionFlag("The top change to submit")
19317
- }, ({ 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."));
19475
+ }, ({ 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."));
19318
19476
 
19319
19477
  // src/cli/gh/command.ts
19320
19478
  var jjGhCommand = make31("jj gh").pipe(withSubcommands([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jj-octo",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
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": {