From 19281b5524213fb0ee4febfc078b8363df8b4418 Mon Sep 17 00:00:00 2001 From: KunN-21 Date: Tue, 7 Jul 2026 12:01:08 +0700 Subject: [PATCH] feat(rtk): add JS-native git-log filter (#2423) Compress git log output via dedicated RTK filter: keep commit headers, Author/Date, subject; drop body padding, decoration, embedded diff lines. Wire into autodetect (git-log prioritized before git-diff) and registry. Co-authored-by: Cursor --- open-sse/rtk/autodetect.js | 7 +- open-sse/rtk/constants.js | 1 + open-sse/rtk/filters/gitLog.js | 99 +++++++++ open-sse/rtk/registry.js | 2 + .../unit/buildOutputFilterAdversarial.test.js | 36 +++ tests/unit/rtk.test.js | 205 ++++++++++++++++-- 6 files changed, 329 insertions(+), 21 deletions(-) create mode 100644 open-sse/rtk/filters/gitLog.js diff --git a/open-sse/rtk/autodetect.js b/open-sse/rtk/autodetect.js index 99ab6a77..8bc4356c 100644 --- a/open-sse/rtk/autodetect.js +++ b/open-sse/rtk/autodetect.js @@ -1,9 +1,10 @@ // Port of auto_detect_filter (rtk/src/cmds/system/pipe_cmd.rs:132-188) + JS extras -// Order: git-diff → git-status → build-output → grep → find → tree → ls → search-list -// → read-numbered → dedup-log → smart-truncate → null +// Detection order: git-log → git-diff → git-status → build-output → grep → find → tree → ls → search-list +// → read-numbered → dedup-log → smart-truncate → null import { DETECT_WINDOW, READ_NUMBERED_MIN_HIT_RATIO, SMART_TRUNCATE_MIN_LINES } from "./constants.js"; import { gitDiff } from "./filters/gitDiff.js"; import { gitStatus } from "./filters/gitStatus.js"; +import { gitLog } from "./filters/gitLog.js"; import { buildOutput } from "./filters/buildOutput.js"; import { grep } from "./filters/grep.js"; import { find } from "./filters/find.js"; @@ -17,6 +18,7 @@ import { searchList, SEARCH_LIST_HEADER_RE } from "./filters/searchList.js"; const RE_GIT_DIFF = /^diff --git /m; const RE_GIT_DIFF_HUNK = /^@@ /m; const RE_GIT_STATUS = /^On branch |^nothing to commit|^Changes (not |to be )|^Untracked files:/m; +const RE_GIT_LOG = /^[*|/\\ ]*commit [0-9a-f]{7,40}$/m; const RE_PORCELAIN = /^[ MADRCU?!][ MADRCU?!] \S/m; const RE_BUILD_OUTPUT = /^(npm (warn|error|ERR!)|yarn (warn|error)|\s*Compiling\s+\S+|\s*Downloading\s+\S+|added \d+ package|\[ERROR\]|BUILD (SUCCESS|FAILED)|\s*Finished\s+|Successfully (installed|built)|ERROR:)/im; const RE_TREE_GLYPH = /[├└]──|│ /; @@ -27,6 +29,7 @@ export function autoDetectFilter(text) { // Rust: floor_char_boundary to avoid UTF-8 split — JS .slice() by char is safe const head = text.length > DETECT_WINDOW ? text.slice(0, DETECT_WINDOW) : text; + if (RE_GIT_LOG.test(head)) return gitLog; if (RE_GIT_DIFF.test(head) || RE_GIT_DIFF_HUNK.test(head)) return gitDiff; if (RE_GIT_STATUS.test(head)) return gitStatus; diff --git a/open-sse/rtk/constants.js b/open-sse/rtk/constants.js index 752c2fee..bc80c23a 100644 --- a/open-sse/rtk/constants.js +++ b/open-sse/rtk/constants.js @@ -4,6 +4,7 @@ export const MIN_COMPRESS_SIZE = 500; // bytes; skip tiny blobs export const DETECT_WINDOW = 1024; // autodetect peeks first N chars export const GIT_DIFF_HUNK_MAX_LINES = 100; // per-hunk line cap export const GIT_DIFF_CONTEXT_KEEP = 3; // context lines around changes +export const GIT_LOG_MAX_LINES = 200; // gitLog line cap export const DEDUP_LINE_MAX = 2000; // dedupLog truncation cap // Rust pipe_cmd.rs parity caps diff --git a/open-sse/rtk/filters/gitLog.js b/open-sse/rtk/filters/gitLog.js new file mode 100644 index 00000000..9769c6de --- /dev/null +++ b/open-sse/rtk/filters/gitLog.js @@ -0,0 +1,99 @@ +// JS-native git-log filter +// Compresses `git log` output: keeps commit headers, subjects, Author/Date; +// drops body padding, decoration, embedded diff lines. +import { GIT_LOG_MAX_LINES } from "../constants.js"; + +export function gitLog(text, maxLines = GIT_LOG_MAX_LINES) { + if (!text) return ""; + + const input = String(text); + const lines = input.split("\n"); + const out = []; + let skipped = 0; + let inCommit = false; + let subjectSeen = false; + + function pushLine(l) { + if (out.length < maxLines) { + out.push(l); + return true; + } + skipped++; + return false; + } + + for (let i = 0; i < lines.length; i++) { + const raw = lines[i]; + const line = raw.trimEnd(); + const trimmed = line.trim(); + + // commit header — starts new commit entry + // Also matched with leading graph decoration (`* commit abc1234...` — --graph without --oneline) + if (/^commit [0-9a-f]{7,40}$/i.test(trimmed) || /^[*|/\\ ]+commit [0-9a-f]{7,40}/i.test(trimmed)) { + inCommit = true; + subjectSeen = false; + pushLine(line); + continue; + } + + if (inCommit) { + // Author / Date — keep as-is (already column 0 in raw, or graph-prefix stripped by commit-header match) + if (/^[*|/\\ ]*(Author|Date):/i.test(trimmed)) { + pushLine(trimmed); + continue; + } + // blank — skip + if (trimmed === "") continue; + // indented subject (4 spaces, optionally preceded by graph decoration) — first one is subject + if (!subjectSeen && /^[*|/\\ ]* \S/.test(line)) { + pushLine(" Subject: " + trimmed); + subjectSeen = true; + continue; + } + // stat summary: "N file(s) changed, N insertions(+), N deletions(-)" + if (/^\d+ file\w* changed/.test(trimmed)) { + pushLine(" " + trimmed); + continue; + } + // embedded diff header — one-line marker + if (/^diff --git /.test(trimmed)) { + pushLine(" ... diff body omitted"); + continue; + } + // everything else in commit body — drop + continue; + } + + // Not in a commit block (--oneline / --graph modes): + + // Graph decoration + sha + subject: "*|/\\ " + const graphMatch = trimmed.match(/^[*|/\\ ]+([0-9a-f]{7,40}\s+.+)/i); + if (graphMatch) { + pushLine(graphMatch[1]); + continue; + } + + // Plain oneline: " " + if (/^[0-9a-f]{7,40}\s+/.test(trimmed)) { + pushLine(trimmed); + continue; + } + + // Pure graph decoration (no sha) — drop + if (/^[*|/\\ ]+$/.test(trimmed) && /[*|/\\]/.test(trimmed)) { + continue; + } + + // catch-all pass-through + pushLine(trimmed); + } + + if (skipped > 0) out.push(`... (${skipped} more lines)`); + + const result = out.join("\n"); + if (!result && input) return input; + if (result.length > input.length) return input; + return result; +} + +gitLog.filterName = "git-log"; diff --git a/open-sse/rtk/registry.js b/open-sse/rtk/registry.js index d9d9bf56..5378aabd 100644 --- a/open-sse/rtk/registry.js +++ b/open-sse/rtk/registry.js @@ -1,6 +1,7 @@ import { FILTERS } from "./constants.js"; import { gitDiff } from "./filters/gitDiff.js"; import { gitStatus } from "./filters/gitStatus.js"; +import { gitLog } from "./filters/gitLog.js"; import { grep } from "./filters/grep.js"; import { find } from "./filters/find.js"; import { dedupLog } from "./filters/dedupLog.js"; @@ -13,6 +14,7 @@ import { searchList } from "./filters/searchList.js"; const REGISTRY = { [FILTERS.GIT_DIFF]: gitDiff, [FILTERS.GIT_STATUS]: gitStatus, + [FILTERS.GIT_LOG]: gitLog, [FILTERS.GREP]: grep, [FILTERS.FIND]: find, [FILTERS.DEDUP_LOG]: dedupLog, diff --git a/tests/unit/buildOutputFilterAdversarial.test.js b/tests/unit/buildOutputFilterAdversarial.test.js index 60193f8c..799f5033 100644 --- a/tests/unit/buildOutputFilterAdversarial.test.js +++ b/tests/unit/buildOutputFilterAdversarial.test.js @@ -4,6 +4,7 @@ import { describe, it, expect } from "vitest"; import { autoDetectFilter } from "../../open-sse/rtk/autodetect.js"; import { buildOutput } from "../../open-sse/rtk/filters/buildOutput.js"; import { gitDiff } from "../../open-sse/rtk/filters/gitDiff.js"; +import { gitLog } from "../../open-sse/rtk/filters/gitLog.js"; import { gitStatus } from "../../open-sse/rtk/filters/gitStatus.js"; import { safeApply } from "../../open-sse/rtk/applyFilter.js"; import { compressMessages } from "../../open-sse/rtk/index.js"; @@ -279,6 +280,41 @@ describe("PR #1175 - integration with compressMessages", () => { }); }); +// ============================================================ +// 6.5. GIT-LOG PRIORITY +// ============================================================ +describe("git-log priority", () => { + it("git-log chosen over build-output when commit header present in first window", () => { + const input = [ + "commit abc1234def5678abc1234def5678abc1234def5", + "Author: Dev One ", + "Date: Sun Jul 6 10:00:00 2026 +0700", + "", + " Add auth middleware", + "", + "diff --git a/src/auth.js b/src/auth.js", + "index abc..def 100644", + "--- a/src/auth.js", + "+++ b/src/auth.js", + "@@ -1 +1 @@", + "+new line" + ].join("\n"); + expect(autoDetectFilter(input)).toBe(gitLog); + }); + + it("pure git diff still stays git-diff", () => { + const input = [ + "diff --git a/src/auth.js b/src/auth.js", + "index abc..def 100644", + "--- a/src/auth.js", + "+++ b/src/auth.js", + "@@ -1 +1 @@", + "+new line" + ].join("\n"); + expect(autoDetectFilter(input)).toBe(gitDiff); + }); +}); + // ============================================================ // 7. PORCELAIN REGRESSION DEEPER TESTS // ============================================================ diff --git a/tests/unit/rtk.test.js b/tests/unit/rtk.test.js index 17d0f1c6..c8009df6 100644 --- a/tests/unit/rtk.test.js +++ b/tests/unit/rtk.test.js @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach } from "vitest"; -import { compressMessages, setRtkEnabled, isRtkEnabled, formatRtkLog } from "../../open-sse/rtk/index.js"; +import { compressMessages, formatRtkLog } from "../../open-sse/rtk/index.js"; import { gitDiff } from "../../open-sse/rtk/filters/gitDiff.js"; import { gitStatus } from "../../open-sse/rtk/filters/gitStatus.js"; import { grep } from "../../open-sse/rtk/filters/grep.js"; @@ -10,6 +10,7 @@ import { tree } from "../../open-sse/rtk/filters/tree.js"; import { smartTruncate } from "../../open-sse/rtk/filters/smartTruncate.js"; import { readNumbered } from "../../open-sse/rtk/filters/readNumbered.js"; import { searchList } from "../../open-sse/rtk/filters/searchList.js"; +import { gitLog } from "../../open-sse/rtk/filters/gitLog.js"; import { autoDetectFilter } from "../../open-sse/rtk/autodetect.js"; import { safeApply } from "../../open-sse/rtk/applyFilter.js"; @@ -53,13 +54,172 @@ function makeFindOutput() { return lines.join("\n"); } -describe("RTK flag", () => { - it("default off, toggle works", () => { - setRtkEnabled(false); - expect(isRtkEnabled()).toBe(false); - setRtkEnabled(true); - expect(isRtkEnabled()).toBe(true); - setRtkEnabled(false); +function makeGitLogOneline() { + return [ + "abc1234 Add auth middleware", + "def5678 Fix token refresh race", + "fedcba9 Update docs" + ].join("\n"); +} + +function makeGitLogDefault() { + return [ + "commit abc1234def5678abc1234def5678abc1234def5", + "Author: Dev One ", + "Date: Sun Jul 6 10:00:00 2026 +0700", + "", + " Add auth middleware", + "", + " More body detail should be dropped.", + " This is padding that consumes tokens." + ].join("\n"); +} + +function makeGitLogGraph() { + return [ + "* abc1234 Add auth middleware", + "| * def5678 Fix token refresh race", + "|/", + "* fedcba9 Update docs" + ].join("\n"); +} + +function makeGitLogGraphDefault() { + return [ + "* commit abc1234def5678abc1234def5678abc1234def5", + "|\\", + "| * commit def5678abc1234def5678abc1234def5678abc1", + "|/", + "|", + "* commit fedcba9abc1234fedcba9abc1234fedcba9abc1234", + "Author: Dev One ", + "Date: Sun Jul 6 10:00:00 2026 +0700", + "", + " Add auth middleware", + "" + ].join("\n"); +} + +function makeGitLogWithMerge() { + return [ + "commit abc1234def5678abc1234def5678abc1234def5", + "Merge: abc1234 def5678", + "Author: Dev One ", + "Date: Sun Jul 6 10:00:00 2026 +0700", + "", + " Merge branch 'feature'" + ].join("\n"); +} + +function makeGitLogWithStats() { + return [ + "commit abc1234def5678abc1234def5678abc1234def5", + "Author: Dev One ", + "Date: Sun Jul 6 10:00:00 2026 +0700", + "", + " Fix typo", + "", + " 2 files changed, 15 insertions(+), 3 deletions(-)" + ].join("\n"); +} + +function makeGitLogWithEmbeddedDiff() { + return [ + "commit abc1234def5678abc1234def5678abc1234def5", + "Author: Dev One ", + "Date: Sun Jul 6 10:00:00 2026 +0700", + "", + " Fix typo", + "", + "diff --git a/src/main.js b/src/main.js" + ].join("\n"); +} + +describe("gitLog filter", () => { + it("compresses git log --oneline without losing commit subjects", () => { + const input = makeGitLogOneline(); + const out = gitLog(input); + expect(out).toContain("abc1234"); + expect(out).toContain("Add auth middleware"); + expect(out.length).toBeLessThanOrEqual(input.length); + }); + + it("keeps commit header + subject in default git log, drops body detail", () => { + const input = makeGitLogDefault(); + const out = gitLog(input); + expect(out).toContain("commit abc1234def5678abc1234def5678abc1234def5"); + expect(out).toContain("Add auth middleware"); + expect(out).not.toContain("More body detail should be dropped."); + }); + + it("strips graph-only decoration but keeps commit subjects", () => { + const input = makeGitLogGraph(); + const out = gitLog(input); + expect(out).toContain("abc1234 Add auth middleware"); + expect(out).toContain("def5678 Fix token refresh race"); + expect(out).not.toContain("|/"); + }); + + it("returns empty string for empty input", () => { + expect(gitLog("")).toBe(""); + }); + + it("returns empty string for null/undefined input", () => { + expect(gitLog(null)).toBe(""); + expect(gitLog(undefined)).toBe(""); + }); + + it("handles git log --graph without --oneline (graph-prefixed commit headers)", () => { + const input = makeGitLogGraphDefault(); + const out = gitLog(input); + expect(out).toContain("commit abc1234def5678abc1234def5678abc1234def5"); + expect(out).toContain("Add auth middleware"); + // graph decoration dropped, pure-graph branch connectors dropped + expect(out).not.toContain("|\\"); + expect(out).not.toContain("|/"); + }); + + it("drops merge commit line ('Merge: abc1234 def5678')", () => { + const input = makeGitLogWithMerge(); + const out = gitLog(input); + expect(out).toContain("commit abc1234def5678abc1234def5678abc1234def5"); + expect(out).toContain("Merge branch 'feature'"); + // "Merge:" line should be dropped (not in output) + expect(out).not.toContain("Merge:"); + }); + + it("keeps stat-summary lines verbatim", () => { + const input = makeGitLogWithStats(); + const out = gitLog(input); + expect(out).toContain("2 files changed, 15 insertions(+), 3 deletions(-)"); + }); + + it("replaces embedded diff markers with '... diff body omitted'", () => { + const input = makeGitLogWithEmbeddedDiff(); + const out = gitLog(input); + expect(out).toContain("diff body omitted"); + // Original diff line replaced + expect(out).not.toContain("diff --git a/src/main.js b/src/main.js"); + }); + + it("truncates beyond maxLines and reports skipped count", () => { + // Generate 50 commit lines but cap at 20 + const lines = []; + for (let i = 0; i < 50; i++) { + lines.push(`commit ${String(i).padStart(40, "0")}`); + } + const input = lines.join("\n"); + const out = gitLog(input, 20); + const outLines = out.split("\n").filter(l => l.length > 0); + expect(outLines.length).toBeLessThanOrEqual(21); // 20 commits + optional skipped note + expect(out).toContain("more lines"); + }); + + it("preserves input when compressed output inflates", () => { + // Input shorter than output would be — e.g. tiny log + const input = "abc\ndef"; + const out = gitLog(input, 10); + expect(out).toBe(input); }); }); @@ -123,6 +283,16 @@ describe("autoDetectFilter", () => { it("detects find", () => { expect(autoDetectFilter("./a/b.js\n./a/c.js\n./a/d.js").filterName).toBe("find"); }); + it("detects git log via commit header", () => { + const input = [ + "commit abc1234def5678abc1234def5678abc1234def5", + "Author: Dev One ", + "Date: Sun Jul 6 10:00:00 2026 +0700", + "", + " Add auth middleware" + ].join("\n"); + expect(autoDetectFilter(input).filterName).toBe("git-log"); + }); it("falls back to dedupLog for generic text", () => { const txt = "line1\nline2\nline3\nline4\nline5\nline6\n"; expect(autoDetectFilter(txt).filterName).toBe("dedup-log"); @@ -245,20 +415,17 @@ describe("safeApply", () => { }); describe("compressMessages (disabled)", () => { - beforeEach(() => setRtkEnabled(false)); it("returns null when disabled", () => { const body = { messages: [{ role: "tool", tool_call_id: "x", content: makeLongDiff() }] }; - expect(compressMessages(body)).toBeNull(); + expect(compressMessages(body, false)).toBeNull(); }); }); describe("compressMessages (enabled)", () => { - beforeEach(() => setRtkEnabled(true)); - it("compresses OpenAI tool message (string content)", () => { const big = makeLongDiff(); const body = { messages: [{ role: "tool", tool_call_id: "call_1", content: big }] }; - const stats = compressMessages(body); + const stats = compressMessages(body, true); expect(stats.hits.length).toBeGreaterThan(0); expect(body.messages[0].content.length).toBeLessThan(big.length); expect(stats.bytesBefore).toBeGreaterThan(stats.bytesAfter); @@ -272,7 +439,7 @@ describe("compressMessages (enabled)", () => { content: [{ type: "tool_result", tool_use_id: "toolu_1", content: big }] }] }; - const stats = compressMessages(body); + const stats = compressMessages(body, true); expect(stats.hits.length).toBeGreaterThan(0); expect(body.messages[0].content[0].content.length).toBeLessThan(big.length); }); @@ -289,7 +456,7 @@ describe("compressMessages (enabled)", () => { }] }] }; - const stats = compressMessages(body); + const stats = compressMessages(body, true); expect(stats.hits.length).toBeGreaterThan(0); expect(body.messages[0].content[0].content[0].text.length).toBeLessThan(big.length); // short part unchanged @@ -304,7 +471,7 @@ describe("compressMessages (enabled)", () => { content: [{ type: "tool_result", tool_use_id: "toolu_1", content: big, is_error: true }] }] }; - const stats = compressMessages(body); + const stats = compressMessages(body, true); expect(stats.hits.length).toBe(0); expect(body.messages[0].content[0].content).toBe(big); }); @@ -312,7 +479,7 @@ describe("compressMessages (enabled)", () => { it("skips below MIN_COMPRESS_SIZE (<500 bytes)", () => { const small = "diff --git a/x b/x\n@@ -1 +1 @@\n+a"; const body = { messages: [{ role: "tool", tool_call_id: "x", content: small }] }; - const stats = compressMessages(body); + const stats = compressMessages(body, true); expect(stats.hits.length).toBe(0); expect(body.messages[0].content).toBe(small); }); @@ -320,7 +487,7 @@ describe("compressMessages (enabled)", () => { it("never produces empty content (R14 guard)", () => { const input = "a".repeat(1000); const body = { messages: [{ role: "tool", tool_call_id: "x", content: input }] }; - compressMessages(body); + compressMessages(body, true); expect(body.messages[0].content.length).toBeGreaterThan(0); }); @@ -339,7 +506,7 @@ describe("compressMessages (enabled)", () => { { role: "user", content: [{ type: "text", text: "next" }] } ] }; - const stats = compressMessages(body); + const stats = compressMessages(body, true); expect(stats).not.toBeNull(); expect(stats.hits.length).toBeGreaterThan(0); });