jj-octo 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -15
- package/dist/jj-octo.js +202 -83
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# jj-octo
|
|
2
2
|
|
|
3
|
-
A [jj](https://github.com/jj-vcs/jj) subcommand for
|
|
3
|
+
A [jj](https://github.com/jj-vcs/jj) subcommand for GitHub that supports stacked pull requests and works in Claude Code cloud sessions.
|
|
4
4
|
|
|
5
5
|
> [!WARNING]
|
|
6
6
|
> `jj-octo` is in early development, and its commands may change. So far it is largely vibe coded.
|
|
@@ -28,31 +28,35 @@ mise use --global github:kkom/jj-octo@X.Y.Z
|
|
|
28
28
|
|
|
29
29
|
Then run `jj-octo init` once in each checkout. It makes the checkout a jj repository colocated with git, makes GitHub's default branch `trunk()`, and sets the `jj gh` alias.
|
|
30
30
|
|
|
31
|
+
Run `jj-octo init` again after upgrading jj-octo, to set up what the new version needs. `jj-octo doctor` reports what isn't set up, and changes nothing.
|
|
32
|
+
|
|
31
33
|
## GitHub access
|
|
32
34
|
|
|
33
|
-
`jj-octo` reads the repository from the `origin` remote.
|
|
35
|
+
`jj-octo` reads the repository from the `origin` remote.
|
|
34
36
|
|
|
35
|
-
|
|
37
|
+
By default, it reads its token from `GH_TOKEN` or `GITHUB_TOKEN`, and otherwise runs `gh auth token`.
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
With `jj-octo.github-auth` set to `git-proxy`, it sends a placeholder token instead. It sends requests through git's proxy for github.com, and trusts git's certificate authorities for it in place of Bun's own. [In a self-hosted environment](#in-a-self-hosted-environment) describes when to use it.
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
## In Claude Code cloud sessions
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
A Claude Code [cloud session](https://code.claude.com/docs/en/claude-code-on-the-web) runs in an Anthropic-hosted environment, or in your organization's self-hosted environment. Each needs its own setup.
|
|
44
|
+
|
|
45
|
+
The session's GitHub proxy can append a "Generated by Claude Code" footer to each pull request body that it creates or edits. `jj gh submit` would read the footer as an edit made on GitHub, and write it into the change. To have `submit` leave the footer out when it compares a pull request's body with its change's description, set:
|
|
44
46
|
|
|
45
|
-
[tools."npm:jj-octo"]
|
|
46
|
-
version = "X.Y.Z"
|
|
47
|
-
# mise holds back a package that is new or rarely downloaded, and this approves it.
|
|
48
|
-
allow_low_downloads = true
|
|
49
47
|
```
|
|
48
|
+
jj config set --repo jj-octo.ignore-claude-code-footer true
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### In an Anthropic-hosted environment
|
|
52
|
+
|
|
53
|
+
There, the [GitHub proxy](https://code.claude.com/docs/en/cloud-environments#github-proxy) carries all GitHub traffic. It serves GitHub API requests only for the repositories attached to the session, and rejects GraphQL requests. That's why `jj-octo` uses only GitHub's REST API, while `gh pr` and other tools that depend on GraphQL fail there.
|
|
50
54
|
|
|
51
|
-
The session sets `HTTPS_PROXY`, `NODE_EXTRA_CA_CERTS
|
|
55
|
+
The session sets `HTTPS_PROXY`, `NODE_EXTRA_CA_CERTS`, and `GH_TOKEN` to the placeholder `proxy-injected`. The GitHub proxy replaces the placeholder with your real credentials. So `jj-octo` works there with its default settings.
|
|
52
56
|
|
|
53
|
-
|
|
57
|
+
### In a self-hosted environment
|
|
54
58
|
|
|
55
|
-
A self-hosted runner started with `--use-anthropic-git-proxy` sends git through Anthropic
|
|
59
|
+
A [self-hosted runner](https://code.claude.com/docs/en/self-hosted-environments-deploy#use-the-anthropic-git-proxy) started with `--use-anthropic-git-proxy` sends git through the Anthropic git proxy. That proxy adds the GitHub credentials of the session's creator. The session then holds no GitHub token, and only git's config names the proxy. There, have `jj-octo` use the same proxy:
|
|
56
60
|
|
|
57
61
|
```
|
|
58
62
|
jj config set --repo jj-octo.github-auth git-proxy
|
package/dist/jj-octo.js
CHANGED
|
@@ -18239,8 +18239,8 @@ var layer14 = /* @__PURE__ */ layerMergedContext(/* @__PURE__ */ succeed6(fetch)
|
|
|
18239
18239
|
// package.json
|
|
18240
18240
|
var package_default = {
|
|
18241
18241
|
name: "jj-octo",
|
|
18242
|
-
version: "0.
|
|
18243
|
-
description: "A jj subcommand for
|
|
18242
|
+
version: "0.6.0",
|
|
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: {
|
|
18246
18246
|
type: "git",
|
|
@@ -18343,12 +18343,30 @@ var gitLayer = ({ cwd, env } = {}) => effect(Git, gen2(function* () {
|
|
|
18343
18343
|
handle.exitCode
|
|
18344
18344
|
], { concurrency: "unbounded" });
|
|
18345
18345
|
return code === 0 ? stdout : yield* new GitFailed({
|
|
18346
|
+
exitCode: code,
|
|
18346
18347
|
message: `git ${args.join(" ")} failed${stderr.trim() === "" ? "" : `: ${stderr.trim()}`}`
|
|
18347
18348
|
});
|
|
18348
18349
|
})).pipe(catchTag2("PlatformError", (error) => fail6(new GitFailed({ message: `git ${args.join(" ")} failed: ${error.message}` }))))
|
|
18349
18350
|
};
|
|
18350
18351
|
}));
|
|
18351
18352
|
var GitLive = gitLayer();
|
|
18353
|
+
var gitConfig = (args) => gen2(function* () {
|
|
18354
|
+
const git = yield* Git;
|
|
18355
|
+
const value = yield* git.read(["config", ...args]).pipe(catch_2((error) => error.exitCode === 1 ? succeed6("") : fail6(error)));
|
|
18356
|
+
return value.trim();
|
|
18357
|
+
});
|
|
18358
|
+
var gitDirectories = gen2(function* () {
|
|
18359
|
+
const git = yield* Git;
|
|
18360
|
+
const [topLevel = "", gitDirectory = "", commonDirectory = ""] = (yield* git.read([
|
|
18361
|
+
"rev-parse",
|
|
18362
|
+
"--path-format=absolute",
|
|
18363
|
+
"--show-toplevel",
|
|
18364
|
+
"--git-dir",
|
|
18365
|
+
"--git-common-dir"
|
|
18366
|
+
])).split(`
|
|
18367
|
+
`);
|
|
18368
|
+
return { commonDirectory, gitDirectory, topLevel };
|
|
18369
|
+
});
|
|
18352
18370
|
|
|
18353
18371
|
// src/clients/jj.ts
|
|
18354
18372
|
class JjFailed extends TaggedError2("JjFailed") {
|
|
@@ -18376,6 +18394,42 @@ var jjLayer = ({ cwd, env, quiet = false } = {}) => effect(Jj, gen2(function* ()
|
|
|
18376
18394
|
return { read, run };
|
|
18377
18395
|
}));
|
|
18378
18396
|
var JjLive = jjLayer();
|
|
18397
|
+
var ENTRY = String.raw`json(name) ++ " " ++ json(value) ++ "\n"`;
|
|
18398
|
+
var readJjConfig = (key, { repo = false } = {}) => gen2(function* () {
|
|
18399
|
+
const jj = yield* Jj;
|
|
18400
|
+
const scope = repo ? ["--repo"] : [];
|
|
18401
|
+
const output = yield* jj.read(["config", "list", ...scope, key, "-T", ENTRY]);
|
|
18402
|
+
const lines = output.split(`
|
|
18403
|
+
`).filter((line) => line !== "");
|
|
18404
|
+
const prefix = `${JSON.stringify(key)} `;
|
|
18405
|
+
if (lines.some((line) => !line.startsWith(prefix))) {
|
|
18406
|
+
return yield* new JjFailed({ message: `${key} is a table; set it to a single value` });
|
|
18407
|
+
}
|
|
18408
|
+
const [line] = lines;
|
|
18409
|
+
return line === undefined ? undefined : JSON.parse(line.slice(prefix.length));
|
|
18410
|
+
});
|
|
18411
|
+
var jjConfig = (key) => gen2(function* () {
|
|
18412
|
+
const value = yield* readJjConfig(key);
|
|
18413
|
+
if (value === undefined || typeof value === "string") {
|
|
18414
|
+
return value ?? "";
|
|
18415
|
+
}
|
|
18416
|
+
return yield* new JjFailed({
|
|
18417
|
+
message: `${key} is ${JSON.stringify(value)}; set it to a string`
|
|
18418
|
+
});
|
|
18419
|
+
});
|
|
18420
|
+
var jjFlag = (key) => gen2(function* () {
|
|
18421
|
+
const value = yield* readJjConfig(key);
|
|
18422
|
+
if (value === undefined || typeof value === "boolean") {
|
|
18423
|
+
return value === true;
|
|
18424
|
+
}
|
|
18425
|
+
return yield* new JjFailed({
|
|
18426
|
+
message: `${key} is ${JSON.stringify(value)}; set it to true or false`
|
|
18427
|
+
});
|
|
18428
|
+
});
|
|
18429
|
+
var setJjConfig = (key, value) => gen2(function* () {
|
|
18430
|
+
const jj = yield* Jj;
|
|
18431
|
+
yield* jj.run(["config", "set", "--repo", key, JSON.stringify(value)]);
|
|
18432
|
+
});
|
|
18379
18433
|
|
|
18380
18434
|
// src/cli/gh/stack/changes.ts
|
|
18381
18435
|
class NotAStack extends TaggedError2("NotAStack") {
|
|
@@ -18497,46 +18551,16 @@ var trunkRevset = (branch) => `${JSON.stringify(branch)}@origin`;
|
|
|
18497
18551
|
var checkTrunk = (branch) => gen2(function* () {
|
|
18498
18552
|
if ((yield* commitsAt(`trunk() & present(${trunkRevset(branch)})`)).length === 0) {
|
|
18499
18553
|
yield* new TrunkMismatch({
|
|
18500
|
-
message: `trunk() isn't ${branch}@origin, GitHub's default branch; run \`jj
|
|
18554
|
+
message: `trunk() isn't ${branch}@origin, GitHub's default branch; run \`jj-octo init\``
|
|
18501
18555
|
});
|
|
18502
18556
|
}
|
|
18503
18557
|
});
|
|
18504
18558
|
|
|
18505
|
-
// src/cli/gh/
|
|
18506
|
-
|
|
18507
|
-
|
|
18508
|
-
|
|
18509
|
-
|
|
18510
|
-
const resolve = (output) => fs.realPath(output.trim()).pipe(orElseSucceed2(() => ""));
|
|
18511
|
-
const root = yield* resolve(yield* git.read(["rev-parse", "--show-toplevel"]));
|
|
18512
|
-
const jjRoot = yield* jj.read(["root"]).pipe(flatMap4(resolve), orElseSucceed2(() => ""));
|
|
18513
|
-
if (jjRoot !== root) {
|
|
18514
|
-
const [gitDirectory, commonDirectory] = (yield* git.read([
|
|
18515
|
-
"rev-parse",
|
|
18516
|
-
"--path-format=absolute",
|
|
18517
|
-
"--git-dir",
|
|
18518
|
-
"--git-common-dir"
|
|
18519
|
-
])).split(`
|
|
18520
|
-
`);
|
|
18521
|
-
if (gitDirectory !== commonDirectory) {
|
|
18522
|
-
yield* new GitFailed({
|
|
18523
|
-
message: "this is a git worktree, where jj would make a second repository; run `jj workspace add` in the main checkout instead"
|
|
18524
|
-
});
|
|
18525
|
-
}
|
|
18526
|
-
yield* jj.run(["git", "init", "--colocate", root]);
|
|
18527
|
-
}
|
|
18528
|
-
});
|
|
18529
|
-
var copyIdentity = gen2(function* () {
|
|
18530
|
-
const git = yield* Git;
|
|
18531
|
-
const jj = yield* Jj;
|
|
18532
|
-
for (const key of ["user.name", "user.email"]) {
|
|
18533
|
-
const own = (yield* jj.read(["config", "get", key])).trim();
|
|
18534
|
-
const value = (yield* git.read(["config", key]).pipe(orElseSucceed2(() => ""))).trim();
|
|
18535
|
-
if (own === "" && value !== "") {
|
|
18536
|
-
yield* jj.run(["config", "set", "--repo", key, JSON.stringify(value)]);
|
|
18537
|
-
}
|
|
18538
|
-
}
|
|
18539
|
-
});
|
|
18559
|
+
// src/cli/gh/setup.ts
|
|
18560
|
+
class NotSetUp extends TaggedError2("NotSetUp") {
|
|
18561
|
+
}
|
|
18562
|
+
var forInit = (message) => ({ byInit: true, message });
|
|
18563
|
+
var advice = ({ byInit, message }) => byInit ? `${message}; run \`jj-octo init\`` : message;
|
|
18540
18564
|
var defaultBranch = gen2(function* () {
|
|
18541
18565
|
const git = yield* Git;
|
|
18542
18566
|
const output = yield* git.read(["ls-remote", "--symref", "origin", "HEAD"]);
|
|
@@ -18546,43 +18570,123 @@ var defaultBranch = gen2(function* () {
|
|
|
18546
18570
|
}
|
|
18547
18571
|
return branch;
|
|
18548
18572
|
});
|
|
18549
|
-
var
|
|
18550
|
-
const
|
|
18551
|
-
const
|
|
18552
|
-
|
|
18553
|
-
|
|
18554
|
-
|
|
18555
|
-
|
|
18556
|
-
|
|
18557
|
-
|
|
18558
|
-
|
|
18559
|
-
|
|
18560
|
-
"
|
|
18561
|
-
|
|
18562
|
-
|
|
18563
|
-
|
|
18564
|
-
|
|
18565
|
-
|
|
18573
|
+
var checkout = gen2(function* () {
|
|
18574
|
+
const fs = yield* FileSystem;
|
|
18575
|
+
const { commonDirectory, gitDirectory, topLevel: root } = yield* gitDirectories;
|
|
18576
|
+
const isColocated = yield* fs.exists(`${root}/.jj`).pipe(orElseSucceed2(() => false));
|
|
18577
|
+
return { isColocated, isWorktree: gitDirectory !== commonDirectory, root };
|
|
18578
|
+
});
|
|
18579
|
+
var colocated = {
|
|
18580
|
+
check: map7(checkout, ({ isColocated, isWorktree }) => {
|
|
18581
|
+
const problem = isWorktree ? {
|
|
18582
|
+
byInit: false,
|
|
18583
|
+
message: "this is a git worktree, where jj would make a second repository; run `jj workspace add` in the main checkout instead"
|
|
18584
|
+
} : forInit("this checkout isn't a jj repository colocated with git");
|
|
18585
|
+
return isColocated ? undefined : problem;
|
|
18586
|
+
}),
|
|
18587
|
+
fix: gen2(function* () {
|
|
18588
|
+
const jj = yield* Jj;
|
|
18589
|
+
const { root } = yield* checkout;
|
|
18590
|
+
yield* jj.run(["git", "init", "--colocate", root]);
|
|
18591
|
+
})
|
|
18592
|
+
};
|
|
18593
|
+
var IDENTITY = ["user.name", "user.email"];
|
|
18594
|
+
var identity2 = (key) => ({
|
|
18595
|
+
check: gen2(function* () {
|
|
18596
|
+
const inJj = (yield* jjConfig(key)) !== "";
|
|
18597
|
+
const inGit = !inJj && (yield* gitConfig([key])) !== "";
|
|
18598
|
+
const problem = inGit ? forInit(`jj has no ${key}, which git has`) : {
|
|
18599
|
+
byInit: false,
|
|
18600
|
+
message: `neither jj nor git has ${key}; set it with \`jj config set --user ${key}\``
|
|
18601
|
+
};
|
|
18602
|
+
return inJj ? undefined : problem;
|
|
18603
|
+
}),
|
|
18604
|
+
fix: gen2(function* () {
|
|
18605
|
+
yield* setJjConfig(key, yield* gitConfig([key]));
|
|
18606
|
+
})
|
|
18566
18607
|
});
|
|
18608
|
+
var ANONYMOUS = 'mutable() & (author_name(exact:"") | author_email(exact:""))';
|
|
18609
|
+
var authors = {
|
|
18610
|
+
check: gen2(function* () {
|
|
18611
|
+
const values = yield* forEach2(IDENTITY, jjConfig);
|
|
18612
|
+
const hasIdentity = values.every((value) => value !== "");
|
|
18613
|
+
const isAnonymous = hasIdentity && (yield* commitsAt(ANONYMOUS)).length > 0;
|
|
18614
|
+
return isAnonymous ? forInit("some mutable changes have no author") : undefined;
|
|
18615
|
+
}),
|
|
18616
|
+
fix: gen2(function* () {
|
|
18617
|
+
const jj = yield* Jj;
|
|
18618
|
+
yield* jj.run(["metaedit", "--quiet", "--update-author", "-r", ANONYMOUS]);
|
|
18619
|
+
})
|
|
18620
|
+
};
|
|
18621
|
+
var isTracked = (branch) => map7(commitsAt(`tracked_remote_bookmarks(exact:${JSON.stringify(branch)}, remote=exact:"origin")`), (commits) => commits.length > 0);
|
|
18622
|
+
var namesTrunk = (branch) => map7(jjConfig('revset-aliases."trunk()"'), (alias) => alias === trunkRevset(branch) || alias === `${branch}@origin`);
|
|
18623
|
+
var trunk = (branchOnOrigin) => ({
|
|
18624
|
+
check: gen2(function* () {
|
|
18625
|
+
const branch = yield* branchOnOrigin;
|
|
18626
|
+
return (yield* isTracked(branch)) && (yield* namesTrunk(branch)) ? undefined : forInit(`trunk() isn't ${branch}@origin, GitHub's default branch, fetched and tracked`);
|
|
18627
|
+
}),
|
|
18628
|
+
fix: gen2(function* () {
|
|
18629
|
+
const jj = yield* Jj;
|
|
18630
|
+
const branch = yield* branchOnOrigin;
|
|
18631
|
+
if ((yield* commitsAt(`present(${trunkRevset(branch)})`)).length === 0) {
|
|
18632
|
+
yield* jj.run(["git", "fetch", "--remote", "origin", "--branch", `exact:${branch}`]);
|
|
18633
|
+
}
|
|
18634
|
+
if (!(yield* isTracked(branch))) {
|
|
18635
|
+
yield* jj.run(["bookmark", "track", `exact:${branch}`, "--remote", "origin"]);
|
|
18636
|
+
}
|
|
18637
|
+
if (!(yield* namesTrunk(branch))) {
|
|
18638
|
+
yield* setJjConfig('revset-aliases."trunk()"', trunkRevset(branch));
|
|
18639
|
+
}
|
|
18640
|
+
})
|
|
18641
|
+
});
|
|
18642
|
+
var alias = {
|
|
18643
|
+
check: map7(readJjConfig("aliases.gh", { repo: true }), (value) => value === undefined ? forInit("`jj gh` isn't set up") : undefined),
|
|
18644
|
+
fix: setJjConfig("aliases.gh", ["util", "exec", "--", "jj-octo"])
|
|
18645
|
+
};
|
|
18646
|
+
var setup = map7(cached2(defaultBranch), (branch) => ({
|
|
18647
|
+
branch,
|
|
18648
|
+
steps: [...IDENTITY.map((key) => identity2(key)), authors, trunk(branch), alias]
|
|
18649
|
+
}));
|
|
18650
|
+
var runSteps = (run) => gen2(function* () {
|
|
18651
|
+
const { branch, steps } = yield* setup;
|
|
18652
|
+
const notColocated = yield* run(colocated);
|
|
18653
|
+
const found = notColocated === undefined ? yield* forEach2(steps, (step) => run(step)) : [notColocated];
|
|
18654
|
+
const problems = new Set(found.filter((problem) => problem !== undefined));
|
|
18655
|
+
return { branch, problems: [...problems] };
|
|
18656
|
+
});
|
|
18657
|
+
|
|
18658
|
+
// src/cli/gh/doctor.ts
|
|
18659
|
+
var find = ({ check }) => check.pipe(map7((problem) => problem === undefined ? undefined : advice(problem)), catch_2((error2) => succeed6(`could not check: ${error2.message}`)));
|
|
18660
|
+
var problems = map7(runSteps(find), ({ problems: found }) => found);
|
|
18661
|
+
var doctor = gen2(function* () {
|
|
18662
|
+
const found = yield* problems;
|
|
18663
|
+
if (found.length > 0) {
|
|
18664
|
+
yield* new NotSetUp({ message: found.join(`
|
|
18665
|
+
`) });
|
|
18666
|
+
}
|
|
18667
|
+
yield* error("set up for jj-octo");
|
|
18668
|
+
});
|
|
18669
|
+
var doctorCommand = make31("doctor", {}, () => asUserError(doctor)).pipe(withHelp("Check that a checkout is set up for the other commands", "Runs the checks of everything `init` sets up or requires, and changes nothing. Fails with each problem, and what fixes it."));
|
|
18670
|
+
|
|
18671
|
+
// src/cli/gh/init.ts
|
|
18672
|
+
var fixed = ({ check, fix }) => gen2(function* () {
|
|
18673
|
+
const found = yield* check;
|
|
18674
|
+
if (found?.byInit !== true) {
|
|
18675
|
+
return found?.message;
|
|
18676
|
+
}
|
|
18677
|
+
yield* fix;
|
|
18678
|
+
const problem = yield* check;
|
|
18679
|
+
return problem?.byInit === true ? `${problem.message}, though init set it up` : problem?.message;
|
|
18680
|
+
}).pipe(catch_2((error2) => succeed6(error2.message)));
|
|
18567
18681
|
var init = gen2(function* () {
|
|
18568
|
-
const
|
|
18569
|
-
|
|
18570
|
-
|
|
18571
|
-
|
|
18572
|
-
if ((yield* commitsAt(anonymous)).length > 0) {
|
|
18573
|
-
yield* jj.run(["metaedit", "--quiet", "--update-author", "-r", anonymous]);
|
|
18682
|
+
const { branch, problems } = yield* runSteps(fixed);
|
|
18683
|
+
if (problems.length > 0) {
|
|
18684
|
+
yield* new NotSetUp({ message: problems.join(`
|
|
18685
|
+
`) });
|
|
18574
18686
|
}
|
|
18575
|
-
|
|
18576
|
-
yield* jj.run([
|
|
18577
|
-
"config",
|
|
18578
|
-
"set",
|
|
18579
|
-
"--repo",
|
|
18580
|
-
"aliases.gh",
|
|
18581
|
-
JSON.stringify(["util", "exec", "--", "jj-octo"])
|
|
18582
|
-
]);
|
|
18583
|
-
yield* error(`set up jj, with trunk() as ${branch}@origin`);
|
|
18687
|
+
yield* error(`set up jj, with trunk() as ${yield* branch}@origin`);
|
|
18584
18688
|
});
|
|
18585
|
-
var initCommand = make31("init", {}, () => asUserError(init)).pipe(withHelp("Set up a git checkout for the other commands", "Makes it a jj repository colocated with git, unless it already is one, and copies git's name and email into the repository's jj config where jj has none.", "Reads the default branch from `origin`, fetches and tracks it, and sets `trunk()` to it in the repository's config. jj's own `trunk()` guesses from branch names, so without this a default branch it doesn't guess would be the base of pull requests but not of local rebases. `submit`, `merge` and `sync` check that the two agree.", "Sets the `jj gh` alias to run `jj-octo` from `PATH`, in the repository's config
|
|
18689
|
+
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."));
|
|
18586
18690
|
|
|
18587
18691
|
// src/clients/github/openapi.ts
|
|
18588
18692
|
var responseOf = () => (schema) => schema;
|
|
@@ -18711,8 +18815,7 @@ var ownToken = gen2(function* () {
|
|
|
18711
18815
|
return stored === "" ? yield* fail6("no token") : make20(stored);
|
|
18712
18816
|
}).pipe(mapError3(() => new GitHubFailed({ message: "no GitHub token: set GH_TOKEN, or run `gh auth login`" })));
|
|
18713
18817
|
var gitSetting = (name) => gen2(function* () {
|
|
18714
|
-
const
|
|
18715
|
-
const value = (yield* git.read(["config", "--get-urlmatch", name, "https://github.com"]).pipe(orElseSucceed2(() => ""))).trim();
|
|
18818
|
+
const value = yield* gitConfig(["--get-urlmatch", name, "https://github.com"]).pipe(mapError3(({ message }) => new GitHubFailed({ message })));
|
|
18716
18819
|
return value === "" ? none2() : some2(value);
|
|
18717
18820
|
});
|
|
18718
18821
|
var gitProxy = gen2(function* () {
|
|
@@ -18735,8 +18838,7 @@ var gitProxy = gen2(function* () {
|
|
|
18735
18838
|
return connection;
|
|
18736
18839
|
});
|
|
18737
18840
|
var connection = gen2(function* () {
|
|
18738
|
-
const
|
|
18739
|
-
const auth = (yield* jj.read(["config", "get", authKey]).pipe(orElseSucceed2(() => ""))).trim();
|
|
18841
|
+
const auth = yield* jjConfig(authKey).pipe(mapError3(({ message }) => new GitHubFailed({ message })));
|
|
18740
18842
|
switch (auth) {
|
|
18741
18843
|
case "":
|
|
18742
18844
|
case "token": {
|
|
@@ -18839,9 +18941,13 @@ var descriptionFrom = ({
|
|
|
18839
18941
|
}) => body === "" ? title : `${title}
|
|
18840
18942
|
|
|
18841
18943
|
${body}`;
|
|
18842
|
-
var
|
|
18944
|
+
var claudeCodeFooter = /\n*---\n_Generated by \[Claude Code\]\(https:\/\/claude\.ai\/code[^\s)]*\)_\s*$/u;
|
|
18945
|
+
var normalizedDescription = (description, { ignoreClaudeCodeFooter }) => {
|
|
18946
|
+
const text = description.replaceAll(`\r
|
|
18843
18947
|
`, `
|
|
18844
|
-
`)
|
|
18948
|
+
`);
|
|
18949
|
+
return descriptionFrom(titleAndBody(ignoreClaudeCodeFooter ? text.replace(claudeCodeFooter, "") : text));
|
|
18950
|
+
};
|
|
18845
18951
|
var descriptionSyncFor = ({
|
|
18846
18952
|
local,
|
|
18847
18953
|
onGitHub,
|
|
@@ -18859,18 +18965,22 @@ var descriptionSyncFor = ({
|
|
|
18859
18965
|
// src/cli/gh/stack/descriptions.ts
|
|
18860
18966
|
class DescriptionConflict extends TaggedError2("DescriptionConflict") {
|
|
18861
18967
|
}
|
|
18968
|
+
var ignoreClaudeCodeFooterKey = "jj-octo.ignore-claude-code-footer";
|
|
18969
|
+
var ignoresClaudeCodeFooter = jjFlag(ignoreClaudeCodeFooterKey);
|
|
18862
18970
|
var descriptionsOf = (commits) => commits.length === 0 ? succeed6(new Map) : readLog(commits.join(" | "), String.raw`commit_id ++ "\t" ++ description.escape_json() ++ "\n"`).pipe(map7((output) => new Map(lines2(output).map((line) => {
|
|
18863
18971
|
const [commit = "", json = '""'] = line.split("\t");
|
|
18864
18972
|
const parsed = JSON.parse(json);
|
|
18865
18973
|
return [commit, typeof parsed === "string" ? parsed : ""];
|
|
18866
18974
|
}))));
|
|
18867
|
-
var syncDescription = ({ bookmark, description }, pullRequest, { overwriteDescriptions, pushedDescription }) => gen2(function* () {
|
|
18975
|
+
var syncDescription = ({ bookmark, description }, pullRequest, { ignoreClaudeCodeFooter, overwriteDescriptions, pushedDescription }) => gen2(function* () {
|
|
18868
18976
|
const git = yield* Git;
|
|
18869
18977
|
const jj = yield* Jj;
|
|
18870
18978
|
const describe = (text) => jj.run(["describe", "-r", bookmarkRevset(bookmark), "-m", text]);
|
|
18871
|
-
const local = normalizedDescription(description);
|
|
18872
|
-
const onGitHub = normalizedDescription(descriptionFrom(pullRequest)
|
|
18873
|
-
|
|
18979
|
+
const local = normalizedDescription(description, { ignoreClaudeCodeFooter });
|
|
18980
|
+
const onGitHub = normalizedDescription(descriptionFrom(pullRequest), {
|
|
18981
|
+
ignoreClaudeCodeFooter
|
|
18982
|
+
});
|
|
18983
|
+
const pushed = normalizedDescription(pushedDescription, { ignoreClaudeCodeFooter });
|
|
18874
18984
|
return yield* value4(descriptionSyncFor({ local, onGitHub, pushed })).pipe(tag3("InSync", () => succeed6(none2())), tag3("TakeGitHub", () => describe(onGitHub).pipe(andThen2(error(`took the description of #${pullRequest.number} from GitHub`)), as2(none2()))), tag3("SendLocal", () => succeed6(some2(local))), tag3("Merge", () => gen2(function* () {
|
|
18875
18985
|
if (overwriteDescriptions) {
|
|
18876
18986
|
return some2(local);
|
|
@@ -18920,12 +19030,14 @@ var publish = (stack, { draft, overwriteDescriptions }) => gen2(function* () {
|
|
|
18920
19030
|
const found = yield* all2(stack.map((change, index) => github.openPullRequest(change.bookmark).pipe(map7((pullRequest) => ({ base: bases[index] ?? trunk, change, pullRequest })))), { concurrency: "unbounded" });
|
|
18921
19031
|
const targets = yield* bookmarkTargets;
|
|
18922
19032
|
const pushedDescriptions = yield* descriptionsOf(bookmarks.flatMap((bookmark) => targets.get(bookmark)?.origin ?? []));
|
|
19033
|
+
const ignoreClaudeCodeFooter = yield* ignoresClaudeCodeFooter;
|
|
18923
19034
|
const changes = [];
|
|
18924
19035
|
for (const entry of found) {
|
|
18925
19036
|
const pushed = targets.get(entry.change.bookmark)?.origin;
|
|
18926
19037
|
changes.push({
|
|
18927
19038
|
...entry,
|
|
18928
19039
|
description: isNone2(entry.pullRequest) ? none2() : yield* syncDescription(entry.change, entry.pullRequest.value, {
|
|
19040
|
+
ignoreClaudeCodeFooter,
|
|
18929
19041
|
overwriteDescriptions,
|
|
18930
19042
|
pushedDescription: (pushed === undefined ? undefined : pushedDescriptions.get(pushed)) ?? ""
|
|
18931
19043
|
})
|
|
@@ -19135,7 +19247,7 @@ var sync4 = (known = []) => gen2(function* () {
|
|
|
19135
19247
|
var syncCommand = make31("sync", {}, () => asUserError(gen2(function* () {
|
|
19136
19248
|
yield* checkTrunk(yield* (yield* GitHub).defaultBranch);
|
|
19137
19249
|
yield* sync4();
|
|
19138
|
-
}).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
|
|
19250
|
+
}).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."));
|
|
19139
19251
|
|
|
19140
19252
|
// src/cli/gh/merge.ts
|
|
19141
19253
|
class NotMergeable extends TaggedError2("NotMergeable") {
|
|
@@ -19205,7 +19317,14 @@ var submitCommand = make31("submit", {
|
|
|
19205
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."));
|
|
19206
19318
|
|
|
19207
19319
|
// src/cli/gh/command.ts
|
|
19208
|
-
var jjGhCommand = make31("jj gh").pipe(withSubcommands([
|
|
19320
|
+
var jjGhCommand = make31("jj gh").pipe(withSubcommands([
|
|
19321
|
+
doctorCommand,
|
|
19322
|
+
initCommand,
|
|
19323
|
+
logCommand,
|
|
19324
|
+
mergeCommand,
|
|
19325
|
+
submitCommand,
|
|
19326
|
+
syncCommand
|
|
19327
|
+
]), withHelp("Commands for publishing jj changes as GitHub pull requests", "A stack is a chain of changes, each published as one pull request on the branch its bookmark names. Each pull request targets the branch of the change below it, so it shows only that change's diff. A single change is a stack of one, and is published as an ordinary pull request.", "The stack of a change is every change connected to it through mutable changes. An empty working copy with no description isn't part of it. `log` shows the whole stack. `submit` and `merge` act on a change and the ones below it, which must be a chain: no merges, no conflicts, and exactly one bookmark on each change.", "The repository is the one the `origin` remote points to. The token is read from `GH_TOKEN` or `GITHUB_TOKEN`, and otherwise from `gh auth token`."));
|
|
19209
19328
|
|
|
19210
19329
|
// src/main.ts
|
|
19211
19330
|
runMain2(run5(jjGhCommand, { version: package_default.version }).pipe(provide3([GitLive, JjLive]), provide3(layer13({
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jj-octo",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A jj subcommand for
|
|
3
|
+
"version": "0.6.0",
|
|
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": {
|
|
7
7
|
"type": "git",
|