@xbghc/warden 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 +36 -14
- package/dist/cli.js +98 -39
- package/dist/web/assets/index-Bi9N5cIP.js +197 -0
- package/dist/web/assets/index-pyKaNQqX.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-D5XV3MyN.js +0 -192
- package/dist/web/assets/index-D5aYUyvz.css +0 -1
package/README.md
CHANGED
|
@@ -4,7 +4,8 @@ Local web UI for reviewing git diffs — built for reviewing changes that a codi
|
|
|
4
4
|
with line comments you can copy back to the agent as a prompt.
|
|
5
5
|
|
|
6
6
|
- Runs as a single local process per repository (`127.0.0.1` only, no auth, no database).
|
|
7
|
-
- Diff sources: working tree, staged, working tree vs HEAD,
|
|
7
|
+
- Diff sources: working tree, staged, working tree vs HEAD, a branch since it forked off its base (commits and
|
|
8
|
+
uncommitted work together), any commit, any two refs, and git worktrees.
|
|
8
9
|
- Side-by-side **Unstaged** and **Staged** file lists, so staging a hunk in your editor is what marks it reviewed.
|
|
9
10
|
- GitHub-style unified / side-by-side diff with syntax highlighting, collapsed file tree, lazy per-file loading, context expansion, virtual scrolling.
|
|
10
11
|
- Line comments (single line or a dragged range), Markdown, edit / delete.
|
|
@@ -13,8 +14,10 @@ with line comments you can copy back to the agent as a prompt.
|
|
|
13
14
|
- Comments follow the code: they move with a hunk that gets staged, re-attach after the agent edits the file, and are cleaned up once the change is committed.
|
|
14
15
|
- Auto-refresh — warden watches the repository and reloads itself when you edit, stage, commit or switch branches.
|
|
15
16
|
- Local issues (title, Markdown body, open/closed) that link comments.
|
|
16
|
-
- Branch-scoped todos,
|
|
17
|
-
- Commit history browser
|
|
17
|
+
- Branch-scoped todos, each copied to the clipboard on its own — one task at a time for the agent.
|
|
18
|
+
- Commit history browser: grouped by day, branch / tag labels, search by message, sha, author or
|
|
19
|
+
path (each a `git log` on the server, not a filter over the rows already loaded), and a
|
|
20
|
+
first-parent view that folds merged branches into their merge commits.
|
|
18
21
|
- Click a line number to jump to that line in a running nvim instance (WSL2 friendly).
|
|
19
22
|
- Read-only with respect to git: only whitelisted read sub-commands are ever executed.
|
|
20
23
|
|
|
@@ -45,19 +48,38 @@ Several instances on the same repository can run at the same time.
|
|
|
45
48
|
| Working tree, uncommitted (incl. untracked) | `working` | `git diff` + `git diff --no-index /dev/null <file>` |
|
|
46
49
|
| Staged | `staged` | `git diff --cached` |
|
|
47
50
|
| Working tree, everything vs HEAD | `all` | `git diff HEAD` (+ untracked) |
|
|
51
|
+
| Branch, everything since it forked off a base (committed or not) | `base:<ref>` | `git diff $(git merge-base <ref> HEAD)` (+ untracked) |
|
|
48
52
|
| One commit | `commit:<sha>` | `git diff <sha>^ <sha>` |
|
|
49
53
|
| Two refs | `range:<base>..<head>` | `git diff <base>...<head>` |
|
|
50
54
|
| Worktree, working tree | `worktree:<path>:working` | same, run inside the worktree |
|
|
51
55
|
| Worktree, branch vs base | `worktree:<path>:range:<base>..<head>` | same, run inside the worktree |
|
|
56
|
+
| Worktree, everything since base | `worktree:<path>:base:<ref>` | same, run inside the worktree |
|
|
52
57
|
|
|
53
58
|
Refs accept anything git can resolve (`main`, `v1.2`, `HEAD~3`, a sha). `@` is `HEAD`.
|
|
54
59
|
Worktrees are discovered with `git worktree list` and share the review state of the main repository.
|
|
55
60
|
|
|
61
|
+
There is no target switcher in the UI: the sidebar's two blocks *are* the working tree, a commit is
|
|
62
|
+
picked from the *Commits* panel, and the *Branch vs* and *Range* fields at the top of that panel open
|
|
63
|
+
the other two. The sidebar names whatever is under review in its block header and puts a
|
|
64
|
+
*返回工作区* link above it.
|
|
65
|
+
|
|
56
66
|
`working`, `staged` and `all` are the three **local views** of one worktree. While any of them is
|
|
57
67
|
selected the sidebar shows two blocks — Unstaged (`working`) and Staged (`staged`) — instead of a
|
|
58
68
|
single tree, and clicking a file switches to the view it belongs to. A file that is only partly
|
|
59
69
|
staged appears in both. Switching between the three keeps your comments, the draft you are typing
|
|
60
|
-
and the current selection; only the diff is reloaded.
|
|
70
|
+
and the current selection; only the diff is reloaded. `all` has no control of its own (a saved
|
|
71
|
+
`lastTarget` can still restore it; *Branch vs* `@` shows the same diff). Commit, range and `base`
|
|
72
|
+
targets keep the single tree.
|
|
73
|
+
|
|
74
|
+
`base:<ref>` is for a branch a coding agent has been working on, committing as it goes: one tree with
|
|
75
|
+
everything since the branch forked off `<ref>` — the commits plus whatever is still uncommitted or
|
|
76
|
+
untracked. The diff runs against the merge base, so commits that landed on `<ref>` after the fork are
|
|
77
|
+
not listed as reverted (which is what a plain `git diff <ref>` would do). The *Branch vs* field in the
|
|
78
|
+
Commits panel suggests the base for you — the main worktree's branch when a sibling worktree is under
|
|
79
|
+
review, otherwise `main` or `master` — and takes any ref. Unlike the three local views, `base` keeps its own
|
|
80
|
+
pool of comments and a commit never deletes them: the round under review is not over when the agent
|
|
81
|
+
commits, so a comment whose lines changed stays *orphaned*, snippet and all, until you have checked
|
|
82
|
+
the fix and delete or re-attach it.
|
|
61
83
|
|
|
62
84
|
## Keyboard
|
|
63
85
|
|
|
@@ -120,6 +142,9 @@ them — the comment's current view first, then `working`, `staged`, `all`. So:
|
|
|
120
142
|
**deleted** (and unlinked from any issue), because the code they were about is now history.
|
|
121
143
|
Anything still visible in Unstaged or Staged survives, including as a context line.
|
|
122
144
|
|
|
145
|
+
Commit, range and `base` targets each keep their own pool and only ever search themselves, and none of
|
|
146
|
+
them deletes on a moved HEAD.
|
|
147
|
+
|
|
123
148
|
Comment markers in the diff belong to one view; the rail's *全部* tab lists the whole pool and
|
|
124
149
|
*此文件* lists every comment on the open file regardless of which view it currently sits in.
|
|
125
150
|
|
|
@@ -143,19 +168,16 @@ Notes attached to a branch rather than to a line of code, for the things you not
|
|
|
143
168
|
that do not belong in a comment. They are not deleted when you commit. They sit in the right-hand
|
|
144
169
|
rail next to the comments, under the *Todo* tab: type a title and press Enter to add one, expand a
|
|
145
170
|
card to read or edit its Markdown body, filter by status, and switch between the current branch
|
|
146
|
-
and all branches
|
|
147
|
-
|
|
171
|
+
and all branches.
|
|
172
|
+
|
|
173
|
+
Each card has its own *复制* button, and there is deliberately no "copy all": a todo is one task to
|
|
174
|
+
hand to an agent, and the agent it goes to is already working in that branch. So the clipboard gets
|
|
175
|
+
the title and the body — no branch, repo, count or numbering:
|
|
148
176
|
|
|
149
177
|
```markdown
|
|
150
|
-
|
|
151
|
-
Branch: feature/x
|
|
152
|
-
Repo: /home/user/project
|
|
153
|
-
Count: 2
|
|
178
|
+
补充 OrderList 的空状态
|
|
154
179
|
|
|
155
|
-
## 1. 补充 OrderList 的空状态
|
|
156
180
|
描述正文(Markdown)…
|
|
157
|
-
|
|
158
|
-
## 2. useOrder 的依赖数组缺少 id
|
|
159
181
|
```
|
|
160
182
|
|
|
161
183
|
## nvim integration
|
|
@@ -201,7 +223,7 @@ move into the matching scope the first time the file is read.
|
|
|
201
223
|
|
|
202
224
|
Single user, local only. The server binds to `127.0.0.1`, executes git only through
|
|
203
225
|
`execFile('git', [...])` with an argument whitelist (`rev-parse`, `diff`, `show`, `log`, `worktree list`,
|
|
204
|
-
`ls-files`, `status`), refuses option-looking refs and any write-capable flag, and never writes into the
|
|
226
|
+
`ls-files`, `status`, `merge-base`), refuses option-looking refs and any write-capable flag, and never writes into the
|
|
205
227
|
repository. Requests that would need anything else get HTTP 400.
|
|
206
228
|
|
|
207
229
|
## Development
|
package/dist/cli.js
CHANGED
|
@@ -2897,6 +2897,11 @@ function parseSuffix(rest) {
|
|
|
2897
2897
|
if (!isValidRef(sha)) throw new TargetKeyError(`invalid commit ref: ${sha}`);
|
|
2898
2898
|
return { kind: "commit", sha };
|
|
2899
2899
|
}
|
|
2900
|
+
if (rest.startsWith("base:")) {
|
|
2901
|
+
const ref = rest.slice("base:".length);
|
|
2902
|
+
if (!isValidRef(ref)) throw new TargetKeyError(`invalid base ref: ${ref}`);
|
|
2903
|
+
return { kind: "base", ref };
|
|
2904
|
+
}
|
|
2900
2905
|
if (rest.startsWith("range:")) {
|
|
2901
2906
|
const spec = rest.slice("range:".length);
|
|
2902
2907
|
const idx = spec.indexOf("..");
|
|
@@ -2912,7 +2917,7 @@ function parseTargetKey(key) {
|
|
|
2912
2917
|
if (typeof key !== "string" || !key) throw new TargetKeyError("empty target key");
|
|
2913
2918
|
if (key.startsWith("worktree:")) {
|
|
2914
2919
|
const rest = key.slice("worktree:".length);
|
|
2915
|
-
const m = /^(.+):(working|staged|all|commit:.+|range:.+)$/.exec(rest);
|
|
2920
|
+
const m = /^(.+):(working|staged|all|commit:.+|range:.+|base:.+)$/.exec(rest);
|
|
2916
2921
|
if (!m || !m[1] || !m[2]) throw new TargetKeyError(`invalid worktree target key: ${key}`);
|
|
2917
2922
|
const worktree = m[1];
|
|
2918
2923
|
if (!worktree.startsWith("/")) throw new TargetKeyError(`worktree path must be absolute: ${worktree}`);
|
|
@@ -2934,6 +2939,9 @@ function formatTargetKey(t) {
|
|
|
2934
2939
|
case "range":
|
|
2935
2940
|
suffix = `range:${t.base}..${t.head}`;
|
|
2936
2941
|
break;
|
|
2942
|
+
case "base":
|
|
2943
|
+
suffix = `base:${t.ref}`;
|
|
2944
|
+
break;
|
|
2937
2945
|
}
|
|
2938
2946
|
return t.worktree ? `worktree:${t.worktree}:${suffix}` : suffix;
|
|
2939
2947
|
}
|
|
@@ -2980,9 +2988,44 @@ function notFound(message, code = "not_found") {
|
|
|
2980
2988
|
return new HttpError(404, message, code);
|
|
2981
2989
|
}
|
|
2982
2990
|
|
|
2991
|
+
// packages/server/src/commits.ts
|
|
2992
|
+
var SEP = "";
|
|
2993
|
+
var COMMIT_FORMAT = `%H${SEP}%h${SEP}%an${SEP}%ae${SEP}%aI${SEP}%P${SEP}%D${SEP}%s`;
|
|
2994
|
+
function parseDecorations(raw2) {
|
|
2995
|
+
const refs = [];
|
|
2996
|
+
let head = false;
|
|
2997
|
+
for (const part of raw2.split(", ")) {
|
|
2998
|
+
const p = part.trim();
|
|
2999
|
+
if (!p || p === "grafted" || p === "replaced") continue;
|
|
3000
|
+
if (p.endsWith("/HEAD")) continue;
|
|
3001
|
+
if (p === "HEAD") head = true;
|
|
3002
|
+
else if (p.startsWith("HEAD -> ")) {
|
|
3003
|
+
head = true;
|
|
3004
|
+
refs.unshift({ name: p.slice("HEAD -> ".length), kind: "branch" });
|
|
3005
|
+
} else if (p.startsWith("tag: ")) refs.push({ name: p.slice("tag: ".length), kind: "tag" });
|
|
3006
|
+
else refs.push({ name: p, kind: "branch" });
|
|
3007
|
+
}
|
|
3008
|
+
return { refs, head };
|
|
3009
|
+
}
|
|
3010
|
+
function parseCommitLog(stdout) {
|
|
3011
|
+
return stdout.split("\n").filter(Boolean).map((line) => {
|
|
3012
|
+
const [sha = "", shortSha = "", author = "", email = "", date = "", parents = "", decorations = "", ...rest] = line.split(SEP);
|
|
3013
|
+
return {
|
|
3014
|
+
sha,
|
|
3015
|
+
shortSha,
|
|
3016
|
+
author,
|
|
3017
|
+
email,
|
|
3018
|
+
date,
|
|
3019
|
+
parents: parents.split(" ").filter(Boolean),
|
|
3020
|
+
...parseDecorations(decorations),
|
|
3021
|
+
subject: rest.join(SEP)
|
|
3022
|
+
};
|
|
3023
|
+
});
|
|
3024
|
+
}
|
|
3025
|
+
|
|
2983
3026
|
// packages/server/src/git.ts
|
|
2984
3027
|
import { execFile } from "child_process";
|
|
2985
|
-
var ALLOWED_SUBCOMMANDS = /* @__PURE__ */ new Set(["rev-parse", "diff", "show", "log", "worktree", "ls-files", "status"]);
|
|
3028
|
+
var ALLOWED_SUBCOMMANDS = /* @__PURE__ */ new Set(["rev-parse", "diff", "show", "log", "worktree", "ls-files", "status", "merge-base"]);
|
|
2986
3029
|
var FORBIDDEN_OPTION_PREFIXES = ["--output", "--ext-diff", "--textconv", "-c", "--config-env", "--exec-path", "--git-dir", "--work-tree"];
|
|
2987
3030
|
var DEFAULT_GIT_TIMEOUT_MS = 3e4;
|
|
2988
3031
|
var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
@@ -3077,6 +3120,14 @@ async function revParse(cwd, ref) {
|
|
|
3077
3120
|
return void 0;
|
|
3078
3121
|
}
|
|
3079
3122
|
}
|
|
3123
|
+
async function mergeBase(cwd, a, b) {
|
|
3124
|
+
try {
|
|
3125
|
+
const r = await runGit(["merge-base", a, b], { cwd });
|
|
3126
|
+
return r.stdout.trim() || void 0;
|
|
3127
|
+
} catch {
|
|
3128
|
+
return void 0;
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3080
3131
|
|
|
3081
3132
|
// packages/server/src/repo.ts
|
|
3082
3133
|
import path from "path";
|
|
@@ -3150,11 +3201,16 @@ async function currentBranch(cwd) {
|
|
|
3150
3201
|
const sha = head.stdout.trim();
|
|
3151
3202
|
return sha ? sha.slice(0, 7) : "HEAD";
|
|
3152
3203
|
}
|
|
3204
|
+
async function detectDefaultBase(cwd) {
|
|
3205
|
+
for (const ref of ["main", "master"]) if (await refExists(cwd, ref)) return ref;
|
|
3206
|
+
return void 0;
|
|
3207
|
+
}
|
|
3153
3208
|
async function getRepoInfo(ctx, defaultTarget) {
|
|
3154
|
-
const [branch, headRes, worktrees] = await Promise.all([
|
|
3209
|
+
const [branch, headRes, worktrees, defaultBase] = await Promise.all([
|
|
3155
3210
|
currentBranch(ctx.root),
|
|
3156
3211
|
runGit(["rev-parse", "HEAD"], { cwd: ctx.root }).catch(() => ({ stdout: "" })),
|
|
3157
|
-
listWorktrees(ctx)
|
|
3212
|
+
listWorktrees(ctx),
|
|
3213
|
+
detectDefaultBase(ctx.root)
|
|
3158
3214
|
]);
|
|
3159
3215
|
return {
|
|
3160
3216
|
root: ctx.root,
|
|
@@ -3162,7 +3218,8 @@ async function getRepoInfo(ctx, defaultTarget) {
|
|
|
3162
3218
|
branch,
|
|
3163
3219
|
head: headRes.stdout.trim(),
|
|
3164
3220
|
worktrees,
|
|
3165
|
-
defaultTarget
|
|
3221
|
+
defaultTarget,
|
|
3222
|
+
...defaultBase ? { defaultBase } : {}
|
|
3166
3223
|
};
|
|
3167
3224
|
}
|
|
3168
3225
|
|
|
@@ -3410,6 +3467,13 @@ function resolveTargetContext(repo, worktrees, key) {
|
|
|
3410
3467
|
async function hasHead(cwd) {
|
|
3411
3468
|
return refExists(cwd, "HEAD");
|
|
3412
3469
|
}
|
|
3470
|
+
async function baseSha(ctx, ref) {
|
|
3471
|
+
if (!await refExists(ctx.cwd, ref)) throw badRequest(`unknown ref: ${ref}`, "unknown_ref");
|
|
3472
|
+
if (!await hasHead(ctx.cwd)) return EMPTY_TREE_SHA;
|
|
3473
|
+
const sha = await mergeBase(ctx.cwd, ref, "HEAD");
|
|
3474
|
+
if (!sha) throw badRequest(`${ref} and HEAD share no history`, "no_merge_base");
|
|
3475
|
+
return sha;
|
|
3476
|
+
}
|
|
3413
3477
|
async function diffArgs(ctx) {
|
|
3414
3478
|
const t = ctx.target;
|
|
3415
3479
|
switch (t.kind) {
|
|
@@ -3429,10 +3493,12 @@ async function diffArgs(ctx) {
|
|
|
3429
3493
|
if (!await refExists(ctx.cwd, t.head)) throw badRequest(`unknown ref: ${t.head}`, "unknown_ref");
|
|
3430
3494
|
return [...DIFF_BASE_ARGS, `${t.base}...${t.head}`];
|
|
3431
3495
|
}
|
|
3496
|
+
case "base":
|
|
3497
|
+
return [...DIFF_BASE_ARGS, await baseSha(ctx, t.ref)];
|
|
3432
3498
|
}
|
|
3433
3499
|
}
|
|
3434
3500
|
function includesUntracked(t) {
|
|
3435
|
-
return t.kind === "working" || t.kind === "all";
|
|
3501
|
+
return t.kind === "working" || t.kind === "all" || t.kind === "base";
|
|
3436
3502
|
}
|
|
3437
3503
|
async function listUntracked(cwd) {
|
|
3438
3504
|
const r = await runGit(["ls-files", "--others", "--exclude-standard", "-z"], { cwd });
|
|
@@ -3512,6 +3578,8 @@ async function refForSide(ctx, side) {
|
|
|
3512
3578
|
return await revParse(ctx.cwd, `${t.sha}^`) ?? EMPTY_TREE_SHA;
|
|
3513
3579
|
case "range":
|
|
3514
3580
|
return side === "new" ? t.head : t.base;
|
|
3581
|
+
case "base":
|
|
3582
|
+
return side === "new" ? void 0 : baseSha(ctx, t.ref);
|
|
3515
3583
|
}
|
|
3516
3584
|
}
|
|
3517
3585
|
async function getFullFile(ctx, filePath, side) {
|
|
@@ -3892,16 +3960,6 @@ function formatIssueExport({ repoRoot, issue, comments }) {
|
|
|
3892
3960
|
parts.push(...comments.map(formatCommentSection));
|
|
3893
3961
|
return parts.join("\n\n") + "\n";
|
|
3894
3962
|
}
|
|
3895
|
-
function formatTodosExport({ repoRoot, branch, todos }) {
|
|
3896
|
-
const head = ["# TODO", `Branch: ${branch}`, `Repo: ${repoRoot}`, `Count: ${todos.length}`];
|
|
3897
|
-
const sections = todos.map((t, i) => {
|
|
3898
|
-
const title = `## ${i + 1}. ${t.title}${t.status === "done" ? " (done)" : ""}`;
|
|
3899
|
-
const body = t.body.replace(/\r\n/g, "\n").trim();
|
|
3900
|
-
return body ? `${title}
|
|
3901
|
-
${body}` : title;
|
|
3902
|
-
});
|
|
3903
|
-
return [head.join("\n"), ...sections].join("\n\n") + "\n";
|
|
3904
|
-
}
|
|
3905
3963
|
|
|
3906
3964
|
// packages/server/src/nvim.ts
|
|
3907
3965
|
import os2 from "os";
|
|
@@ -4653,15 +4711,6 @@ function createApp(opts) {
|
|
|
4653
4711
|
});
|
|
4654
4712
|
return c.json(todo, 201);
|
|
4655
4713
|
});
|
|
4656
|
-
api.post("/todos/export", async (c) => {
|
|
4657
|
-
const body = await c.req.json();
|
|
4658
|
-
const branch = typeof body.branch === "string" ? body.branch.trim() : "";
|
|
4659
|
-
if (!branch) throw badRequest("branch is required");
|
|
4660
|
-
const s = await store.load();
|
|
4661
|
-
const todos = s.todos.filter((t) => t.branch === branch && (body.includeDone === true || t.status === "open"));
|
|
4662
|
-
const res = { text: formatTodosExport({ repoRoot: repo.root, branch, todos }), count: todos.length };
|
|
4663
|
-
return c.json(res);
|
|
4664
|
-
});
|
|
4665
4714
|
api.patch("/todos/:id", async (c) => {
|
|
4666
4715
|
const id = c.req.param("id");
|
|
4667
4716
|
const body = await c.req.json();
|
|
@@ -4689,23 +4738,29 @@ function createApp(opts) {
|
|
|
4689
4738
|
api.get("/commits", async (c) => {
|
|
4690
4739
|
const limitRaw = Number(c.req.query("limit") ?? 200);
|
|
4691
4740
|
const limit = Math.min(Math.max(Number.isFinite(limitRaw) ? Math.floor(limitRaw) : 200, 1), 1e3);
|
|
4692
|
-
const
|
|
4741
|
+
const offsetRaw = Number(c.req.query("offset") ?? 0);
|
|
4742
|
+
const offset = Number.isFinite(offsetRaw) && offsetRaw > 0 ? Math.floor(offsetRaw) : 0;
|
|
4693
4743
|
const filePath = c.req.query("path") || void 0;
|
|
4694
4744
|
const ref = c.req.query("ref") || void 0;
|
|
4695
4745
|
const rootParam = c.req.query("root") || void 0;
|
|
4746
|
+
const q = (c.req.query("q") ?? "").trim();
|
|
4747
|
+
const author = (c.req.query("author") ?? "").trim();
|
|
4748
|
+
const firstParent = c.req.query("firstParent") === "1";
|
|
4696
4749
|
let cwd = repo.root;
|
|
4697
4750
|
if (rootParam) {
|
|
4698
4751
|
const wt = (await worktrees()).find((w) => w.path === rootParam);
|
|
4699
4752
|
if (!wt && rootParam !== repo.root) throw badRequest("unknown root", "unknown_worktree");
|
|
4700
4753
|
cwd = rootParam;
|
|
4701
4754
|
}
|
|
4702
|
-
if (before && !isValidRef(before)) throw badRequest("invalid before ref");
|
|
4703
4755
|
if (ref && !isValidRef(ref)) throw badRequest("invalid ref");
|
|
4704
4756
|
if (filePath && (filePath.startsWith("/") || filePath.split("/").includes(".."))) throw badRequest("invalid path");
|
|
4705
|
-
|
|
4706
|
-
const args = ["log", `--max-count=${limit + 1}`, `--
|
|
4707
|
-
if (
|
|
4708
|
-
|
|
4757
|
+
if (q.length > 200 || author.length > 200) throw badRequest("search text too long");
|
|
4758
|
+
const args = ["log", `--max-count=${limit + 1}`, `--skip=${offset}`, `--format=${COMMIT_FORMAT}`];
|
|
4759
|
+
if (firstParent) args.push("--first-parent");
|
|
4760
|
+
if (q || author) args.push("--fixed-strings", "--regexp-ignore-case");
|
|
4761
|
+
if (q) args.push(`--grep=${q}`);
|
|
4762
|
+
if (author) args.push(`--author=${author}`);
|
|
4763
|
+
if (ref) args.push(ref);
|
|
4709
4764
|
if (filePath) args.push("--", filePath);
|
|
4710
4765
|
let stdout = "";
|
|
4711
4766
|
try {
|
|
@@ -4714,13 +4769,17 @@ function createApp(opts) {
|
|
|
4714
4769
|
if (e instanceof HttpError && e.code === "git_failed") stdout = "";
|
|
4715
4770
|
else throw e;
|
|
4716
4771
|
}
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
})
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4772
|
+
const rows = parseCommitLog(stdout);
|
|
4773
|
+
const hasMore = rows.length > limit;
|
|
4774
|
+
let commits = rows.slice(0, limit);
|
|
4775
|
+
if (q && offset === 0 && /^[0-9a-f]{4,40}$/i.test(q)) {
|
|
4776
|
+
const sha = await revParse(cwd, `${q}^{commit}`);
|
|
4777
|
+
if (sha && !commits.some((x) => x.sha === sha)) {
|
|
4778
|
+
const hit = parseCommitLog((await runGit(["log", "--max-count=1", `--format=${COMMIT_FORMAT}`, sha], { cwd })).stdout)[0];
|
|
4779
|
+
if (hit) commits = [hit, ...commits];
|
|
4780
|
+
}
|
|
4781
|
+
}
|
|
4782
|
+
const res = { commits, hasMore };
|
|
4724
4783
|
return c.json(res);
|
|
4725
4784
|
});
|
|
4726
4785
|
api.get("/events", async (c) => {
|
|
@@ -4900,7 +4959,7 @@ async function startServer(opts) {
|
|
|
4900
4959
|
}
|
|
4901
4960
|
|
|
4902
4961
|
// bin/cli.ts
|
|
4903
|
-
var VERSION = true ? "0.
|
|
4962
|
+
var VERSION = true ? "0.6.0" : "dev";
|
|
4904
4963
|
function parseArgs(argv) {
|
|
4905
4964
|
const args = { repoPath: process.cwd(), open: true, help: false, version: false };
|
|
4906
4965
|
for (let i = 0; i < argv.length; i++) {
|