jj-octo 0.6.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 (2) hide show
  1. package/dist/jj-octo.js +401 -251
  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.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: {
@@ -18439,6 +18439,7 @@ var SCRATCH = '(working_copies() & empty() & description(exact:""))';
18439
18439
  var stackRevset = (revision) => `(reachable(${revision}, mutable()) & (${STACK_ROOTS})::) ~ ${SCRATCH}`;
18440
18440
  var downstackRevset = (revision) => `::(${revision}) & (${stackRevset(revision)})`;
18441
18441
  var upstackRevset = (revision) => `(${revision})+:: & (${stackRevset(revision)})`;
18442
+ var anyOf = (revsets) => ["none()", ...revsets].join(" | ");
18442
18443
  var bookmarkRevset = (bookmark) => `bookmarks(exact:"${bookmark}")`;
18443
18444
  var lines2 = (output) => output.split(`
18444
18445
  `).filter((line) => line !== "");
@@ -18544,6 +18545,121 @@ var chain = (revset, revision) => gen2(function* () {
18544
18545
  var stackChanges = (revision) => chain(stackRevset(revision), revision);
18545
18546
  var downstackChanges = (revision) => chain(downstackRevset(revision), revision);
18546
18547
 
18548
+ // src/clients/github/openapi.ts
18549
+ var responseOf = () => (schema) => schema;
18550
+ var bodyOf = (body) => bodyJsonUnsafe(body);
18551
+
18552
+ // src/clients/github/types.ts
18553
+ var PullRequestNumberSchema = Number6.pipe(brand("PullRequestNumber"));
18554
+ var StackNumberSchema = Number6.pipe(brand("StackNumber"));
18555
+
18556
+ // src/clients/github/client.ts
18557
+ class GitHubFailed extends TaggedError2("GitHubFailed") {
18558
+ }
18559
+
18560
+ class GitHub extends Service()("jj-octo/GitHub") {
18561
+ }
18562
+ var CreatedPullRequest = responseOf()(Struct({ number: PullRequestNumberSchema }));
18563
+ var CreatedStack = responseOf()(Struct({ number: StackNumberSchema }));
18564
+ var PullRequests = responseOf()(ArraySchema(Struct({
18565
+ base: Struct({ ref: String4 }),
18566
+ body: NullOr(String4),
18567
+ draft: optionalKey2(Boolean3),
18568
+ number: PullRequestNumberSchema,
18569
+ title: String4
18570
+ })));
18571
+ var ClosedPullRequests = responseOf()(ArraySchema(Struct({
18572
+ head: Struct({ sha: String4 }),
18573
+ merged_at: NullOr(String4)
18574
+ })));
18575
+ var StackedPullRequest = Struct({
18576
+ number: PullRequestNumberSchema,
18577
+ state: Literals(["closed", "open"])
18578
+ });
18579
+ var Stacks = responseOf()(ArraySchema(Struct({ number: StackNumberSchema, pull_requests: ArraySchema(StackedPullRequest) })));
18580
+ var RepositoryDetails = responseOf()(Struct({ default_branch: String4 }));
18581
+ var MergeRequest = Struct({
18582
+ details: Struct({
18583
+ message: String4,
18584
+ uuid: optionalKey2(String4)
18585
+ }),
18586
+ status: Literals(["enqueued", "failed", "merged", "pending"])
18587
+ });
18588
+ var StartedMerge = responseOf()(MergeRequest);
18589
+ var MergeResult = responseOf()(MergeRequest);
18590
+ var GitHubLive = (repository, connection) => {
18591
+ const repo = (path) => `/repos/${repository.owner}/${repository.name}${path}`;
18592
+ return effect(GitHub, gen2(function* () {
18593
+ const requestInit = match(connection.proxy, {
18594
+ onNone: () => ({}),
18595
+ onSome: ({ ca, url }) => match(ca, {
18596
+ onNone: () => ({ proxy: url }),
18597
+ onSome: (certificates) => ({ proxy: url, tls: { ca: certificates } })
18598
+ })
18599
+ });
18600
+ const client = (yield* HttpClient).pipe(mapRequest((request) => request.pipe(prependUrl("https://api.github.com"), acceptJson, bearerToken(connection.token), setHeaders({
18601
+ "user-agent": "jj-octo",
18602
+ "x-github-api-version": "2022-11-28"
18603
+ }))));
18604
+ 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({
18605
+ message: `${request.method} ${request.url} returned ${response.status}: ${text}`
18606
+ }))))), catchTag2("HttpClientError", (error) => fail6(new GitHubFailed({ message: error.message }))));
18607
+ const json = (schema) => (request) => send(request).pipe(flatMap4(schemaBodyJson(schema)), catchTags2({
18608
+ HttpClientError: (error) => fail6(new GitHubFailed({ message: error.message })),
18609
+ SchemaError: (error) => fail6(new GitHubFailed({
18610
+ message: `unexpected response to ${request.url}: ${error.message}`
18611
+ }))
18612
+ }));
18613
+ const stackOf = (pullRequest) => json(Stacks)(get4(repo(`/stacks?pull_request=${pullRequest}`))).pipe(map7((stacks) => map(fromNullishOr(stacks[0]), (stack) => ({
18614
+ number: stack.number,
18615
+ open: stack.pull_requests.filter(({ state }) => state === "open").map(({ number }) => number)
18616
+ }))));
18617
+ const mergeRequest = (path) => json(MergeResult)(get4(repo(path)));
18618
+ const fromBranch = (branch, state) => repo(`/pulls?state=${state}&head=${repository.owner}:${encodeURIComponent(branch)}`);
18619
+ return {
18620
+ addToStack: (stack, pullRequests) => send(post(repo(`/stacks/${stack}/add`)).pipe(bodyOf({ pull_requests: [...pullRequests] }))).pipe(asVoid2),
18621
+ createPullRequest: ({ base, body, draft, head, title }) => json(CreatedPullRequest)(post(repo("/pulls")).pipe(bodyOf({ base, body, draft, head, title }))).pipe(map7(({ number }) => number)),
18622
+ createStack: (pullRequests) => json(CreatedStack)(post(repo("/stacks")).pipe(bodyOf({ pull_requests: [...pullRequests] }))).pipe(map7(({ number }) => number)),
18623
+ defaultBranch: json(RepositoryDetails)(get4(repo(""))).pipe(map7(({ default_branch }) => default_branch)),
18624
+ mergePullRequest: (pullRequest) => send(put(repo(`/pulls/${pullRequest}/merge`)).pipe(bodyOf({ merge_method: "squash" }))).pipe(asVoid2),
18625
+ mergeStack: (pullRequest) => gen2(function* () {
18626
+ const started = yield* json(StartedMerge)(put(repo(`/pulls/${pullRequest}/merge-async`)).pipe(bodyOf({
18627
+ merge_action: "default",
18628
+ merge_method: "squash"
18629
+ })));
18630
+ const { uuid } = started.details;
18631
+ if (uuid === undefined) {
18632
+ return yield* new GitHubFailed({
18633
+ message: `merging #${pullRequest} returned no request to follow`
18634
+ });
18635
+ }
18636
+ const settled = yield* mergeRequest(`/pulls/${pullRequest}/merge-async/${uuid}`).pipe(repeat2({
18637
+ schedule: spaced("5 seconds"),
18638
+ until: ({ status }) => status !== "pending"
18639
+ }));
18640
+ if (settled.status === "merged" || settled.status === "enqueued") {
18641
+ return settled.status;
18642
+ }
18643
+ return yield* new GitHubFailed({
18644
+ message: `merging #${pullRequest} ended ${settled.status}: ${settled.details.message}`
18645
+ });
18646
+ }),
18647
+ mergedHeads: (branch) => json(ClosedPullRequests)(get4(fromBranch(branch, "closed"))).pipe(map7((closed) => closed.filter(({ merged_at }) => merged_at !== null).map(({ head }) => head.sha))),
18648
+ openPullRequest: (branch) => json(PullRequests)(get4(fromBranch(branch, "open"))).pipe(map7((found) => map(fromNullishOr(found[0]), ({ base, body, draft, number, title }) => ({
18649
+ base: base.ref,
18650
+ body: body ?? "",
18651
+ draft: draft ?? false,
18652
+ number,
18653
+ title
18654
+ })))),
18655
+ setBase: (pullRequest, base) => send(patch(repo(`/pulls/${pullRequest}`)).pipe(bodyOf({ base }))).pipe(asVoid2),
18656
+ setDescription: (pullRequest, { body, title }) => send(patch(repo(`/pulls/${pullRequest}`)).pipe(bodyOf({ body, title }))).pipe(asVoid2),
18657
+ stackOf,
18658
+ unstack: (stack) => send(post(repo(`/stacks/${stack}/unstack`))).pipe(asVoid2)
18659
+ };
18660
+ }));
18661
+ };
18662
+
18547
18663
  // src/cli/gh/stack/trunk.ts
18548
18664
  class TrunkMismatch extends TaggedError2("TrunkMismatch") {
18549
18665
  }
@@ -18555,6 +18671,11 @@ var checkTrunk = (branch) => gen2(function* () {
18555
18671
  });
18556
18672
  }
18557
18673
  });
18674
+ var checkedTrunk = gen2(function* () {
18675
+ const branch = yield* (yield* GitHub).defaultBranch;
18676
+ yield* checkTrunk(branch);
18677
+ return branch;
18678
+ });
18558
18679
 
18559
18680
  // src/cli/gh/setup.ts
18560
18681
  class NotSetUp extends TaggedError2("NotSetUp") {
@@ -18688,121 +18809,6 @@ var init = gen2(function* () {
18688
18809
  });
18689
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."));
18690
18811
 
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
18812
  // src/clients/github/connection.ts
18807
18813
  var authKey = "jj-octo.github-auth";
18808
18814
  var ownToken = gen2(function* () {
@@ -19003,115 +19009,24 @@ var syncDescription = ({ bookmark, description }, pullRequest, { ignoreClaudeCod
19003
19009
  })), exhaustive2);
19004
19010
  });
19005
19011
 
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* () {
19012
+ // src/cli/gh/stack/combine.ts
19013
+ var pushedOf = (bookmarks) => bookmarks.filter(({ remote }) => remote !== undefined);
19014
+ var bookmarksBeforeFetch = (revset) => gen2(function* () {
19110
19015
  const targets = [...yield* bookmarkTargets];
19111
- 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"));
19112
19019
  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 }));
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
+ }));
19115
19030
  });
19116
19031
  });
19117
19032
  var localCommit = (change, remote, fetched) => gen2(function* () {
@@ -19122,22 +19037,31 @@ var localCommit = (change, remote, fetched) => gen2(function* () {
19122
19037
  }
19123
19038
  return local;
19124
19039
  });
19125
- var combine4 = ({ change, local: localBefore, name, remote }, fetched) => gen2(function* () {
19040
+ var combine4 = ({ above, change, editing, local: localBefore, name, remote }, fetched) => gen2(function* () {
19126
19041
  const jj = yield* Jj;
19127
19042
  if (remote === undefined || fetched === undefined || fetched === remote) {
19128
19043
  return true;
19129
19044
  }
19130
19045
  const [local, ...others] = yield* localCommit(change, remote, fetched);
19131
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
+ }
19132
19055
  return true;
19133
19056
  }
19134
19057
  if (others.length > 0) {
19135
19058
  return false;
19136
19059
  }
19137
19060
  const keepLocal = jj.run(["bookmark", "set", name, "-r", local, "--allow-backwards"]);
19061
+ const held = `::(bookmarks() ~ ${bookmarkRevset(name)}) ~ ::${remote}`;
19138
19062
  const dropCopies = jj.run([
19139
19063
  "abandon",
19140
- `((::${fetched} | ::${remote}) & mutable()) ~ ::${bookmarkRevset(name)}`
19064
+ `((::${fetched} | ::${remote}) & mutable()) ~ ::${bookmarkRevset(name)} ~ (${held})`
19141
19065
  ]);
19142
19066
  if ((yield* commitsAt(`${remote} & ::${fetched}`)).length > 0) {
19143
19067
  yield* keepLocal;
@@ -19155,6 +19079,9 @@ var combine4 = ({ change, local: localBefore, name, remote }, fetched) => gen2(f
19155
19079
  }
19156
19080
  if (localBefore === remote) {
19157
19081
  yield* jj.run(["rebase", "--source", `children(${local})`, "--destination", fetched]);
19082
+ if (editing) {
19083
+ yield* jj.run(["edit", fetched]);
19084
+ }
19158
19085
  yield* jj.run(["abandon", local]);
19159
19086
  yield* error(`took GitHub's rewritten ${name}, which wasn't changed locally`);
19160
19087
  return true;
@@ -19169,6 +19096,9 @@ var combine4 = ({ change, local: localBefore, name, remote }, fetched) => gen2(f
19169
19096
  return false;
19170
19097
  });
19171
19098
 
19099
+ // src/cli/gh/stack/reconcile.ts
19100
+ var BEHIND_ROOTS = `(${STACK_ROOTS}) ~ children(trunk())`;
19101
+
19172
19102
  class NotCombined extends TaggedError2("NotCombined") {
19173
19103
  }
19174
19104
  var rebaseStack = (root, required) => gen2(function* () {
@@ -19188,8 +19118,8 @@ var rebaseStack = (root, required) => gen2(function* () {
19188
19118
  String.raw`id ++ "\n"`
19189
19119
  ]));
19190
19120
  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(" | ");
19121
+ const rebased = anyOf(changes.map((change) => `change_id(${change})`));
19122
+ const untouched = anyOf(conflictedCommits);
19193
19123
  const conflictedNow = yield* changeIdsAt(`(conflicts() & (${rebased})) ~ (${untouched})`);
19194
19124
  const mayRestore = !changes.some((change) => required.includes(change));
19195
19125
  if (mayRestore && conflictedNow.some((change) => !conflicted.includes(change))) {
@@ -19197,23 +19127,49 @@ var rebaseStack = (root, required) => gen2(function* () {
19197
19127
  yield* error(`left "${name}" and the changes on it where they were: they conflict with the trunk`);
19198
19128
  }
19199
19129
  });
19200
- var mergedOnGitHub = (bookmarks) => gen2(function* () {
19130
+ var notMerged = (merged) => (name) => merged.every(({ bookmark }) => bookmark !== name);
19131
+ var mergedHeadsOf = (bookmarks) => gen2(function* () {
19201
19132
  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();
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] ?? []]));
19204
19138
  });
19205
- var sync4 = (known = []) => gen2(function* () {
19139
+ var mergedIn = (bookmarks, heads) => gen2(function* () {
19206
19140
  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}`]);
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
+ ]);
19215
19158
  }
19216
- 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()`;
19217
19173
  const required = yield* changeIdsAt(`(${mergedRevset}):: ~ (${mergedRevset})`);
19218
19174
  if (merged.length > 0) {
19219
19175
  yield* jj.run(["abandon", mergedRevset]);
@@ -19221,18 +19177,20 @@ var sync4 = (known = []) => gen2(function* () {
19221
19177
  }
19222
19178
  const fetched = yield* bookmarkTargets;
19223
19179
  const notCombined = [];
19224
- for (const bookmark of before.filter(({ name }) => merged.every((change) => change.bookmark !== name))) {
19180
+ for (const bookmark of before.filter(({ name }) => notMerged(merged)(name))) {
19225
19181
  if (!(yield* combine4(bookmark, fetched.get(bookmark.name)?.origin))) {
19226
19182
  notCombined.push(bookmark.name);
19227
19183
  }
19228
19184
  }
19229
- 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)) {
19230
19190
  yield* rebaseStack(root, required);
19231
19191
  }
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
- }
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)));
19236
19194
  if (notCombined.length > 0) {
19237
19195
  yield* new NotCombined({
19238
19196
  message: [
@@ -19244,8 +19202,201 @@ var sync4 = (known = []) => gen2(function* () {
19244
19202
  });
19245
19203
  }
19246
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
+ });
19247
19398
  var syncCommand = make31("sync", {}, () => asUserError(gen2(function* () {
19248
- yield* checkTrunk(yield* (yield* GitHub).defaultBranch);
19399
+ yield* checkedTrunk;
19249
19400
  yield* sync4();
19250
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."));
19251
19402
 
@@ -19281,8 +19432,7 @@ var mergeOnGitHub = ({ base, number }, trunk) => gen2(function* () {
19281
19432
  return "merged";
19282
19433
  });
19283
19434
  var merge5 = (revision) => gen2(function* () {
19284
- const trunk = yield* (yield* GitHub).defaultBranch;
19285
- yield* checkTrunk(trunk);
19435
+ const trunk = yield* checkedTrunk;
19286
19436
  const changes = yield* downstackChanges(revision);
19287
19437
  const top = changes.at(-1)?.bookmark ?? "";
19288
19438
  const targets = yield* bookmarkTargets;
@@ -19301,7 +19451,7 @@ var merge5 = (revision) => gen2(function* () {
19301
19451
  yield* sync4(changes);
19302
19452
  for (const bookmark of above) {
19303
19453
  if ((yield* commitsAt(bookmarkRevset(bookmark))).length > 0) {
19304
- yield* republish(bookmarkRevset(bookmark));
19454
+ yield* republish(bookmarkRevset(bookmark), trunk);
19305
19455
  break;
19306
19456
  }
19307
19457
  }
@@ -19314,7 +19464,7 @@ var submitCommand = make31("submit", {
19314
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")),
19315
19465
  publish: Boolean6("publish").pipe(withDefault5(false), withDescription2("Open new pull requests ready for review, instead of as drafts")),
19316
19466
  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."));
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."));
19318
19468
 
19319
19469
  // src/cli/gh/command.ts
19320
19470
  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.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": {