jj-octo 0.2.4 → 0.3.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.
- package/README.md +8 -0
- package/dist/jj-octo.js +95 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -56,6 +56,14 @@ bun update --global --latest jj-octo
|
|
|
56
56
|
|
|
57
57
|
It uses only GitHub's REST API, so that it works in a [Claude Code](https://claude.com/claude-code) cloud session. There, GitHub is reached through a proxy that allows REST calls for the session's repository and blocks GraphQL, which `gh pr` depends on.
|
|
58
58
|
|
|
59
|
+
Where git reaches github.com through a proxy that adds the credentials itself, as in a Claude Code cloud session that keeps the token outside the sandbox, have `jj-octo` use that proxy too:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
jj config set --repo jj-octo.github-auth git-proxy
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`jj-octo` then sends its requests through the `http.proxy` that git uses for `https://github.com`, trusts the certificate authorities in git's `http.sslCAInfo` for it, and sends a placeholder token that the proxy replaces. It reads git's settings on each run, so it follows a proxy that moves to another port. `token`, the default, sends your own token.
|
|
66
|
+
|
|
59
67
|
## Development
|
|
60
68
|
|
|
61
69
|
```
|
package/dist/jj-octo.js
CHANGED
|
@@ -18112,7 +18112,7 @@ var layer14 = /* @__PURE__ */ layerMergedContext(/* @__PURE__ */ succeed6(fetch)
|
|
|
18112
18112
|
// package.json
|
|
18113
18113
|
var package_default = {
|
|
18114
18114
|
name: "jj-octo",
|
|
18115
|
-
version: "0.
|
|
18115
|
+
version: "0.3.1",
|
|
18116
18116
|
description: "A jj subcommand for working with GitHub that supports stacking",
|
|
18117
18117
|
license: "MIT",
|
|
18118
18118
|
repository: {
|
|
@@ -18478,6 +18478,10 @@ var PullRequests = responseOf()(ArraySchema(Struct({
|
|
|
18478
18478
|
number: PullRequestNumberSchema,
|
|
18479
18479
|
title: String4
|
|
18480
18480
|
})));
|
|
18481
|
+
var ClosedPullRequests = responseOf()(ArraySchema(Struct({
|
|
18482
|
+
head: Struct({ sha: String4 }),
|
|
18483
|
+
merged_at: NullOr(String4)
|
|
18484
|
+
})));
|
|
18481
18485
|
var StackedPullRequest = Struct({
|
|
18482
18486
|
number: PullRequestNumberSchema,
|
|
18483
18487
|
state: Literals(["closed", "open"])
|
|
@@ -18493,24 +18497,21 @@ var MergeRequest = Struct({
|
|
|
18493
18497
|
});
|
|
18494
18498
|
var StartedMerge = responseOf()(MergeRequest);
|
|
18495
18499
|
var MergeResult = responseOf()(MergeRequest);
|
|
18496
|
-
var
|
|
18497
|
-
const fromEnvironment = yield* option2(Redacted2("GH_TOKEN").pipe(orElse(() => Redacted2("GITHUB_TOKEN"))));
|
|
18498
|
-
if (isSome2(fromEnvironment)) {
|
|
18499
|
-
return fromEnvironment.value;
|
|
18500
|
-
}
|
|
18501
|
-
const spawner = yield* ChildProcessSpawner;
|
|
18502
|
-
const stored = (yield* spawner.string(make14("gh", ["auth", "token"]))).trim();
|
|
18503
|
-
return stored === "" ? yield* fail6("no token") : make19(stored);
|
|
18504
|
-
}).pipe(mapError2(() => new GitHubFailed({ message: "no GitHub token: set GH_TOKEN, or run `gh auth login`" })));
|
|
18505
|
-
var GitHubLive = (repository) => {
|
|
18500
|
+
var GitHubLive = (repository, connection) => {
|
|
18506
18501
|
const repo = (path) => `/repos/${repository.owner}/${repository.name}${path}`;
|
|
18507
18502
|
return effect(GitHub, gen2(function* () {
|
|
18508
|
-
const
|
|
18509
|
-
|
|
18503
|
+
const requestInit = match(connection.proxy, {
|
|
18504
|
+
onNone: () => ({}),
|
|
18505
|
+
onSome: ({ ca, url }) => match(ca, {
|
|
18506
|
+
onNone: () => ({ proxy: url }),
|
|
18507
|
+
onSome: (certificates) => ({ proxy: url, tls: { ca: certificates } })
|
|
18508
|
+
})
|
|
18509
|
+
});
|
|
18510
|
+
const client = (yield* HttpClient).pipe(mapRequest((request) => request.pipe(prependUrl("https://api.github.com"), acceptJson, bearerToken(connection.token), setHeaders({
|
|
18510
18511
|
"user-agent": "jj-octo",
|
|
18511
18512
|
"x-github-api-version": "2022-11-28"
|
|
18512
18513
|
}))));
|
|
18513
|
-
const send = (request) => client.execute(request).pipe(flatMap4((response) => response.status < 400 ? succeed6(response) : response.text.pipe(orElseSucceed2(() => ""), flatMap4((text) => fail6(new GitHubFailed({
|
|
18514
|
+
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({
|
|
18514
18515
|
message: `${request.method} ${request.url} returned ${response.status}: ${text}`
|
|
18515
18516
|
}))))), catchTag2("HttpClientError", (error) => fail6(new GitHubFailed({ message: error.message }))));
|
|
18516
18517
|
const json = (schema) => (request) => send(request).pipe(flatMap4(schemaBodyJson(schema)), catchTags2({
|
|
@@ -18524,6 +18525,7 @@ var GitHubLive = (repository) => {
|
|
|
18524
18525
|
open: stack.pull_requests.filter(({ state }) => state === "open").map(({ number }) => number)
|
|
18525
18526
|
}))));
|
|
18526
18527
|
const mergeRequest = (path) => json(MergeResult)(get4(repo(path)));
|
|
18528
|
+
const fromBranch = (branch, state) => repo(`/pulls?state=${state}&head=${repository.owner}:${encodeURIComponent(branch)}`);
|
|
18527
18529
|
return {
|
|
18528
18530
|
addToStack: (stack, pullRequests) => send(post(repo(`/stacks/${stack}/add`)).pipe(bodyOf({ pull_requests: [...pullRequests] }))).pipe(asVoid2),
|
|
18529
18531
|
createPullRequest: ({ base, body, draft, head, title }) => json(CreatedPullRequest)(post(repo("/pulls")).pipe(bodyOf({ base, body, draft, head, title }))).pipe(map7(({ number }) => number)),
|
|
@@ -18552,7 +18554,8 @@ var GitHubLive = (repository) => {
|
|
|
18552
18554
|
message: `merging #${pullRequest} ended ${settled.status}: ${settled.details.message}`
|
|
18553
18555
|
});
|
|
18554
18556
|
}),
|
|
18555
|
-
|
|
18557
|
+
mergedHeads: (branch) => json(ClosedPullRequests)(get4(fromBranch(branch, "closed"))).pipe(map7((closed) => closed.filter(({ merged_at }) => merged_at !== null).map(({ head }) => head.sha))),
|
|
18558
|
+
openPullRequest: (branch) => json(PullRequests)(get4(fromBranch(branch, "open"))).pipe(map7((found) => map(fromNullishOr(found[0]), ({ base, body, draft, number, title }) => ({
|
|
18556
18559
|
base: base.ref,
|
|
18557
18560
|
body: body ?? "",
|
|
18558
18561
|
draft: draft ?? false,
|
|
@@ -18567,6 +18570,60 @@ var GitHubLive = (repository) => {
|
|
|
18567
18570
|
}));
|
|
18568
18571
|
};
|
|
18569
18572
|
|
|
18573
|
+
// src/clients/github/connection.ts
|
|
18574
|
+
var authKey = "jj-octo.github-auth";
|
|
18575
|
+
var ownToken = gen2(function* () {
|
|
18576
|
+
const fromEnvironment = yield* option2(Redacted2("GH_TOKEN").pipe(orElse(() => Redacted2("GITHUB_TOKEN"))));
|
|
18577
|
+
if (isSome2(fromEnvironment)) {
|
|
18578
|
+
return fromEnvironment.value;
|
|
18579
|
+
}
|
|
18580
|
+
const spawner = yield* ChildProcessSpawner;
|
|
18581
|
+
const stored = (yield* spawner.string(make14("gh", ["auth", "token"]))).trim();
|
|
18582
|
+
return stored === "" ? yield* fail6("no token") : make19(stored);
|
|
18583
|
+
}).pipe(mapError2(() => new GitHubFailed({ message: "no GitHub token: set GH_TOKEN, or run `gh auth login`" })));
|
|
18584
|
+
var gitSetting = (name) => gen2(function* () {
|
|
18585
|
+
const git = yield* Git;
|
|
18586
|
+
const value = (yield* git.read(["config", "--get-urlmatch", name, "https://github.com"]).pipe(orElseSucceed2(() => ""))).trim();
|
|
18587
|
+
return value === "" ? none2() : some2(value);
|
|
18588
|
+
});
|
|
18589
|
+
var gitProxy = gen2(function* () {
|
|
18590
|
+
const fs = yield* FileSystem;
|
|
18591
|
+
const url = yield* gitSetting("http.proxy");
|
|
18592
|
+
if (isNone2(url)) {
|
|
18593
|
+
return yield* new GitHubFailed({
|
|
18594
|
+
message: `${authKey} is git-proxy, but git has no http.proxy for https://github.com`
|
|
18595
|
+
});
|
|
18596
|
+
}
|
|
18597
|
+
const caFile = yield* gitSetting("http.sslCAInfo");
|
|
18598
|
+
const ca = isNone2(caFile) ? none2() : some2(yield* fs.readFileString(caFile.value).pipe(mapError2((error) => new GitHubFailed({
|
|
18599
|
+
message: `can't read git's http.sslCAInfo ${caFile.value}: ${error.message}`
|
|
18600
|
+
}))));
|
|
18601
|
+
const connection = {
|
|
18602
|
+
proxy: some2({ ca, url: url.value }),
|
|
18603
|
+
token: make19("proxy-injected")
|
|
18604
|
+
};
|
|
18605
|
+
return connection;
|
|
18606
|
+
});
|
|
18607
|
+
var connection = gen2(function* () {
|
|
18608
|
+
const jj = yield* Jj;
|
|
18609
|
+
const auth = (yield* jj.read(["config", "get", authKey]).pipe(orElseSucceed2(() => ""))).trim();
|
|
18610
|
+
switch (auth) {
|
|
18611
|
+
case "":
|
|
18612
|
+
case "token": {
|
|
18613
|
+
const own = { proxy: none2(), token: yield* ownToken };
|
|
18614
|
+
return own;
|
|
18615
|
+
}
|
|
18616
|
+
case "git-proxy": {
|
|
18617
|
+
return yield* gitProxy;
|
|
18618
|
+
}
|
|
18619
|
+
default: {
|
|
18620
|
+
return yield* new GitHubFailed({
|
|
18621
|
+
message: `${authKey} is ${JSON.stringify(auth)}; set it to token or git-proxy`
|
|
18622
|
+
});
|
|
18623
|
+
}
|
|
18624
|
+
}
|
|
18625
|
+
});
|
|
18626
|
+
|
|
18570
18627
|
// src/clients/github/repository.ts
|
|
18571
18628
|
var repositoryFromUrl = (url) => {
|
|
18572
18629
|
const match = /github\.com[:/](?<owner>[^/]+)\/(?<name>[^/]+?)(?:\.git)?\/?$/u.exec(url.trim());
|
|
@@ -18585,7 +18642,7 @@ var GitHubFromOrigin = unwrap(gen2(function* () {
|
|
|
18585
18642
|
if (isNone2(repository)) {
|
|
18586
18643
|
return yield* new GitHubFailed({ message: "the `origin` remote isn't a GitHub repository" });
|
|
18587
18644
|
}
|
|
18588
|
-
return GitHubLive(repository.value);
|
|
18645
|
+
return GitHubLive(repository.value, yield* connection);
|
|
18589
18646
|
}));
|
|
18590
18647
|
|
|
18591
18648
|
// src/cli/gh/log.ts
|
|
@@ -18898,20 +18955,31 @@ var rebaseStack = (root, required) => gen2(function* () {
|
|
|
18898
18955
|
yield* error(`left "${name}" and the changes on it where they were: they conflict with the trunk`);
|
|
18899
18956
|
}
|
|
18900
18957
|
});
|
|
18901
|
-
var
|
|
18958
|
+
var mergedOnGitHub = (bookmarks) => gen2(function* () {
|
|
18959
|
+
const github = yield* GitHub;
|
|
18960
|
+
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" });
|
|
18961
|
+
return merged.flat();
|
|
18962
|
+
});
|
|
18963
|
+
var sync4 = (known = []) => gen2(function* () {
|
|
18902
18964
|
const jj = yield* Jj;
|
|
18903
18965
|
const before = yield* bookmarksBeforeFetch;
|
|
18966
|
+
const merged = [
|
|
18967
|
+
...known,
|
|
18968
|
+
...yield* mergedOnGitHub(before.filter(({ name }) => known.every(({ bookmark }) => bookmark !== name)))
|
|
18969
|
+
];
|
|
18904
18970
|
yield* jj.run(["git", "fetch"]);
|
|
18905
|
-
for (const { bookmark } of
|
|
18971
|
+
for (const { bookmark } of merged) {
|
|
18906
18972
|
yield* jj.run(["bookmark", "forget", "--include-remotes", `exact:${bookmark}`]);
|
|
18907
18973
|
}
|
|
18908
|
-
|
|
18909
|
-
|
|
18910
|
-
|
|
18974
|
+
const mergedRevset = `(${["none()", ...merged.map(({ changeId }) => `change_id(${changeId})`)].join(" | ")}) & mutable()`;
|
|
18975
|
+
const required = yield* changeIdsAt(`(${mergedRevset}):: ~ (${mergedRevset})`);
|
|
18976
|
+
if (merged.length > 0) {
|
|
18977
|
+
yield* jj.run(["abandon", mergedRevset]);
|
|
18978
|
+
yield* error(`abandoned the changes whose pull requests merged: ${merged.map(({ bookmark }) => bookmark).join(", ")}`);
|
|
18911
18979
|
}
|
|
18912
18980
|
const fetched = yield* bookmarkTargets;
|
|
18913
18981
|
const notCombined = [];
|
|
18914
|
-
for (const bookmark of before.filter(({ name }) =>
|
|
18982
|
+
for (const bookmark of before.filter(({ name }) => merged.every((change) => change.bookmark !== name))) {
|
|
18915
18983
|
if (!(yield* combine4(bookmark, fetched.get(bookmark.name)?.origin))) {
|
|
18916
18984
|
notCombined.push(bookmark.name);
|
|
18917
18985
|
}
|
|
@@ -18919,8 +18987,8 @@ var sync4 = (required = [], mergedChanges = []) => gen2(function* () {
|
|
|
18919
18987
|
for (const root of yield* commitsAt(BEHIND_ROOTS)) {
|
|
18920
18988
|
yield* rebaseStack(root, required);
|
|
18921
18989
|
}
|
|
18922
|
-
const
|
|
18923
|
-
for (const name of
|
|
18990
|
+
const onTrunk = (yield* bookmarksAt("::trunk()")).filter((name) => before.some((bookmark) => bookmark.name === name));
|
|
18991
|
+
for (const name of onTrunk) {
|
|
18924
18992
|
yield* jj.run(["bookmark", "forget", "--include-remotes", `exact:${name}`]);
|
|
18925
18993
|
}
|
|
18926
18994
|
if (notCombined.length > 0) {
|
|
@@ -18937,7 +19005,7 @@ var sync4 = (required = [], mergedChanges = []) => gen2(function* () {
|
|
|
18937
19005
|
var syncCommand = make30("sync", {}, () => asUserError(gen2(function* () {
|
|
18938
19006
|
yield* checkTrunk(yield* (yield* GitHub).defaultBranch);
|
|
18939
19007
|
yield* sync4();
|
|
18940
|
-
}).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
|
|
19008
|
+
}).pipe(provide3(GitHubFromOrigin)))).pipe(withHelp("Fetch, and rebase every change onto the trunk", "Fetches, then rebases every change that isn't merged yet onto the trunk, keeping each on the changes below it. A change whose pull request GitHub merged is abandoned and its bookmark forgotten, unless it was edited after the merge.", "A stack that would conflict with the trunk is left where it was, and named. Rebase it yourself with `jj rebase` when you want to resolve it. The rest of a stack whose changes merged is rebased even where it conflicts, and the conflict is recorded in the change.", "Commits added to a branch on GitHub are squashed into the branch's local change, whether or not that change was also edited locally. Where GitHub only rebased a branch, the local change is kept.", "Fails before fetching if `trunk()` isn't GitHub's default branch, which `jj gh init` sets.", "Pushes nothing. Run `jj gh submit` to update the pull requests of a change and the ones below it."));
|
|
18941
19009
|
|
|
18942
19010
|
// src/cli/gh/merge.ts
|
|
18943
19011
|
class NotMergeable extends TaggedError2("NotMergeable") {
|
|
@@ -18984,12 +19052,11 @@ var merge5 = (revision) => gen2(function* () {
|
|
|
18984
19052
|
}
|
|
18985
19053
|
const pullRequest = yield* openPullRequestOf(top);
|
|
18986
19054
|
const above = yield* bookmarksAt(upstackRevset(bookmarkRevset(top)));
|
|
18987
|
-
const stackChanges = yield* changeIdsAt(stackRevset(revision));
|
|
18988
19055
|
if ((yield* mergeOnGitHub(pullRequest, trunk)) === "enqueued") {
|
|
18989
19056
|
yield* error("queued to merge; run `jj gh sync` once it has");
|
|
18990
19057
|
} else {
|
|
18991
19058
|
yield* error(`merged up to #${pullRequest.number}`);
|
|
18992
|
-
yield* sync4(
|
|
19059
|
+
yield* sync4(changes);
|
|
18993
19060
|
for (const bookmark of above) {
|
|
18994
19061
|
if ((yield* commitsAt(bookmarkRevset(bookmark))).length > 0) {
|
|
18995
19062
|
yield* republish(bookmarkRevset(bookmark));
|
|
@@ -18998,7 +19065,7 @@ var merge5 = (revision) => gen2(function* () {
|
|
|
18998
19065
|
}
|
|
18999
19066
|
}
|
|
19000
19067
|
});
|
|
19001
|
-
var mergeCommand = make30("merge", { revision: revisionFlag("The top change to merge") }, ({ revision }) => asUserError(merge5(revision).pipe(provide3(GitHubFromOrigin)))).pipe(withHelp("Merge the pull requests of a change and the ones below it", "Squash-merges them, and prints which before it does. An empty working copy means the change below it. Fails before merging if any of the changes has edits that aren't pushed.", "A pull request in a GitHub stack is merged together with the pull requests below it. Either all of them merge or none does. A pull request outside a stack is merged alone, and only if its base is the trunk.", "Then syncs, as `jj gh sync` does
|
|
19068
|
+
var mergeCommand = make30("merge", { revision: revisionFlag("The top change to merge") }, ({ revision }) => asUserError(merge5(revision).pipe(provide3(GitHubFromOrigin)))).pipe(withHelp("Merge the pull requests of a change and the ones below it", "Squash-merges them, and prints which before it does. An empty working copy means the change below it. Fails before merging if any of the changes has edits that aren't pushed.", "A pull request in a GitHub stack is merged together with the pull requests below it. Either all of them merge or none does. A pull request outside a stack is merged alone, and only if its base is the trunk.", "Then syncs, as `jj gh sync` does: it abandons the merged changes, forgets their bookmarks, and rebases the rest of the stack onto the trunk even where it conflicts, recording the conflict in the change. Then it pushes what is left of the change's stack again, up to the highest branch already on GitHub.", "If GitHub queues the merge instead, nothing is synced. Run `jj gh sync` once it has merged."));
|
|
19002
19069
|
|
|
19003
19070
|
// src/cli/gh/submit.ts
|
|
19004
19071
|
var submitCommand = make30("submit", {
|