gityo 1.0.10 → 1.0.11

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 (3) hide show
  1. package/README.md +18 -1
  2. package/dist/index.js +223 -14
  3. package/package.json +6 -2
package/README.md CHANGED
@@ -154,10 +154,27 @@ Example:
154
154
  "autoAcceptMessage": false,
155
155
  "postCommand": "push",
156
156
  "autoRunPostCommand": false,
157
- "instructions": "Write short, clear commit messages."
157
+ "instructions": "Write short, clear commit messages.",
158
+ "maxDiffTokens": 24000,
159
+ "perFileCap": 400
158
160
  }
159
161
  ```
160
162
 
163
+ ### Large changes
164
+
165
+ When the diff is small, it is sent to the model as-is. When it is too large for one request, gityo minimizes it first:
166
+
167
+ - regenerates the diff with minimal context lines
168
+ - drops lock files and minified/generated files from the payload
169
+ - caps each file's patch and lists every changed file with its line counts so the model still sees the full picture
170
+
171
+ If the minimized diff is still too large, each remaining part is summarized in parallel and a final commit message is generated from those summaries.
172
+
173
+ Two optional config knobs control this:
174
+
175
+ - `maxDiffTokens` — estimated token budget for the diff sent to the model (default `24000`)
176
+ - `perFileCap` — max diff lines kept per file when minimizing (default `400`)
177
+
161
178
  Example instructions file:
162
179
 
163
180
  ```md
package/dist/index.js CHANGED
@@ -1710,7 +1710,7 @@ function useColor() {
1710
1710
  if (process$1.env.FORCE_COLOR || process$1.env.CLICOLOR_FORCE !== void 0) return true;
1711
1711
  }
1712
1712
  new Command();
1713
- var version$4 = "1.0.10";
1713
+ var version$4 = "1.0.11";
1714
1714
  Object.freeze({ status: "aborted" });
1715
1715
  function $constructor$3(name$16, initializer$6, params) {
1716
1716
  function init$1(inst, def) {
@@ -175938,6 +175938,8 @@ const configSchema = object$4({
175938
175938
  models: record$3(string$6().min(1), modelSchema),
175939
175939
  autoAcceptMessage: boolean$5(),
175940
175940
  instructions: string$6().min(1),
175941
+ maxDiffTokens: number$5().int().min(1e3),
175942
+ perFileCap: number$5().int().min(50),
175941
175943
  postCommand: _enum$4(["push", "push-and-pull"]).nullable(),
175942
175944
  autoRunPostCommand: boolean$5()
175943
175945
  }).partial();
@@ -175947,6 +175949,8 @@ function resolveConfig(input) {
175947
175949
  models: parsed.models,
175948
175950
  instructions: parsed.instructions,
175949
175951
  autoAcceptCommitMessage: parsed.autoAcceptMessage ?? false,
175952
+ maxDiffTokens: parsed.maxDiffTokens,
175953
+ perFileCap: parsed.perFileCap,
175950
175954
  postCommand: parsed.postCommand === void 0 ? "push" : parsed.postCommand,
175951
175955
  autoRunPostCommand: parsed.autoRunPostCommand ?? false
175952
175956
  };
@@ -181074,17 +181078,23 @@ async function getChangedFiles(git) {
181074
181078
  ...splitNull(untracked)
181075
181079
  ])).sort((left, right) => left.localeCompare(right));
181076
181080
  }
181077
- async function getCommitDiff(git) {
181081
+ async function getCommitDiff(git, contextLines = 3) {
181082
+ const unified = [`-U${contextLines}`];
181078
181083
  const staged = await git.raw([
181079
181084
  "diff",
181080
181085
  "--cached",
181081
- "--no-ext-diff"
181086
+ "--no-ext-diff",
181087
+ ...unified
181082
181088
  ]);
181083
181089
  if (staged.trim().length > 0) return {
181084
181090
  diff: staged,
181085
181091
  hasStaged: true
181086
181092
  };
181087
- const [unstaged, untracked] = await Promise.all([git.raw(["diff", "--no-ext-diff"]), git.raw([
181093
+ const [unstaged, untracked] = await Promise.all([git.raw([
181094
+ "diff",
181095
+ "--no-ext-diff",
181096
+ ...unified
181097
+ ]), git.raw([
181088
181098
  "ls-files",
181089
181099
  "--others",
181090
181100
  "--exclude-standard",
@@ -181094,6 +181104,7 @@ async function getCommitDiff(git) {
181094
181104
  diff: [unstaged, ...await Promise.all(splitNull(untracked).map((file$4) => git.raw([
181095
181105
  "diff",
181096
181106
  "--no-index",
181107
+ ...unified,
181097
181108
  "--",
181098
181109
  "/dev/null",
181099
181110
  file$4
@@ -181112,20 +181123,178 @@ function createLiveGit(baseDir) {
181112
181123
  });
181113
181124
  return git;
181114
181125
  }
181126
+ const DEFAULT_MAX_DIFF_TOKENS = 24e3;
181127
+ const DEFAULT_PER_FILE_CAP = 400;
181128
+ const CHARS_PER_TOKEN = 4;
181129
+ const MINIMIZED_CONTEXT_LINES = 1;
181130
+ const FILE_SECTION_PREFIX = "diff --git ";
181131
+ const HUNK_HEADER_PREFIX = "@@ ";
181132
+ const NOISE_FILE_PATTERNS = [/\.(lock|min\.(?:js|mjs|cjs|css)|map)$/i, /-lock\.[^/]+$/i];
181133
+ function estimateTokens(text$1) {
181134
+ return Math.ceil(text$1.length / CHARS_PER_TOKEN);
181135
+ }
181136
+ async function minimizeDiff(git, options) {
181137
+ const { diff } = await getCommitDiff(git, MINIMIZED_CONTEXT_LINES);
181138
+ const sections = splitFileSections(diff).map(parseSection);
181139
+ const kept = [];
181140
+ const excludedPaths = /* @__PURE__ */ new Set();
181141
+ for (const section of sections) if (isNoiseFile(section.path)) excludedPaths.add(section.path);
181142
+ else kept.push(section);
181143
+ return {
181144
+ toc: buildToc(sections, excludedPaths, options.allFiles),
181145
+ body: kept.map((section) => capSection(section, options.perFileCap)).join("\n")
181146
+ };
181147
+ }
181148
+ function splitDiffIntoChunks(diff, maxTokens) {
181149
+ return packIntoChunks(splitFileSections(diff).flatMap((section) => estimateTokens(section) <= maxTokens ? [section] : splitOversizedSection(section, maxTokens)), maxTokens);
181150
+ }
181151
+ function isNoiseFile(path$4) {
181152
+ return NOISE_FILE_PATTERNS.some((pattern) => pattern.test(path$4));
181153
+ }
181154
+ function splitFileSections(diff) {
181155
+ return ("\n" + diff.trimStart()).split("\ndiff --git ").slice(1).map((part) => FILE_SECTION_PREFIX + part);
181156
+ }
181157
+ function parseSection(section) {
181158
+ let additions = 0;
181159
+ let deletions = 0;
181160
+ let binary = false;
181161
+ for (const line of section.split("\n").slice(1)) {
181162
+ if (line.startsWith("Binary files") || line.startsWith("GIT binary patch")) {
181163
+ binary = true;
181164
+ continue;
181165
+ }
181166
+ if (line.startsWith("+++") || line.startsWith("---")) continue;
181167
+ if (line.startsWith("+")) additions++;
181168
+ else if (line.startsWith("-")) deletions++;
181169
+ }
181170
+ return {
181171
+ path: extractPath(section.split("\n")[0]),
181172
+ content: section,
181173
+ additions,
181174
+ deletions,
181175
+ binary
181176
+ };
181177
+ }
181178
+ function extractPath(header) {
181179
+ const match = header.match(/^diff --git a\/(.+) b\/(.+)$/);
181180
+ return match ? match[2] : "";
181181
+ }
181182
+ function capSection(section, cap) {
181183
+ const lines = section.content.split("\n");
181184
+ const bodyLines = lines.length - 1;
181185
+ if (bodyLines <= cap) return section.content;
181186
+ return [...lines.slice(0, cap + 1), `[... ${bodyLines - cap} more lines truncated]`].join("\n");
181187
+ }
181188
+ function buildToc(sections, excludedPaths, allFiles) {
181189
+ const lines = ["Changed files:"];
181190
+ const seen = /* @__PURE__ */ new Set();
181191
+ for (const section of sections) {
181192
+ seen.add(section.path);
181193
+ if (section.binary) {
181194
+ lines.push(`- ${section.path} (binary)`);
181195
+ continue;
181196
+ }
181197
+ lines.push(`- ${section.path} (+${section.additions} -${section.deletions})`);
181198
+ }
181199
+ for (const path$4 of excludedPaths) {
181200
+ seen.add(path$4);
181201
+ lines.push(`- ${path$4} (excluded generated file)`);
181202
+ }
181203
+ for (const file$4 of allFiles) if (!seen.has(file$4)) lines.push(`- ${file$4}`);
181204
+ return lines.join("\n");
181205
+ }
181206
+ function packIntoChunks(pieces, maxTokens) {
181207
+ const chunks = [];
181208
+ let current = "";
181209
+ for (const piece of pieces) {
181210
+ if (current.length > 0 && estimateTokens(current + piece) > maxTokens) {
181211
+ chunks.push(current);
181212
+ current = "";
181213
+ }
181214
+ current += piece;
181215
+ }
181216
+ if (current.length > 0) chunks.push(current);
181217
+ return chunks;
181218
+ }
181219
+ function splitOversizedSection(section, maxTokens) {
181220
+ const [header, ...lines] = section.split("\n");
181221
+ const hunks = [];
181222
+ let currentHunk = [];
181223
+ for (const line of lines) {
181224
+ if (line.startsWith(HUNK_HEADER_PREFIX) && currentHunk.length > 0) {
181225
+ hunks.push(currentHunk);
181226
+ currentHunk = [];
181227
+ }
181228
+ currentHunk.push(line);
181229
+ }
181230
+ if (currentHunk.length > 0) hunks.push(currentHunk);
181231
+ return packIntoChunks(hunks.flatMap((hunk) => {
181232
+ const piece = [header, ...hunk].join("\n");
181233
+ return estimateTokens(piece) <= maxTokens ? [piece] : hardSplit(piece, maxTokens);
181234
+ }), maxTokens);
181235
+ }
181236
+ function hardSplit(text$1, maxTokens) {
181237
+ const maxChars = maxTokens * CHARS_PER_TOKEN;
181238
+ const chunks = [];
181239
+ let current = "";
181240
+ for (const line of text$1.split("\n")) {
181241
+ if (estimateTokens(line) > maxTokens) {
181242
+ if (current.length > 0) {
181243
+ chunks.push(current);
181244
+ current = "";
181245
+ }
181246
+ for (let index$2 = 0; index$2 < line.length; index$2 += maxChars) chunks.push(line.slice(index$2, index$2 + maxChars));
181247
+ continue;
181248
+ }
181249
+ const candidate = current.length === 0 ? line : `${current}\n${line}`;
181250
+ if (estimateTokens(candidate) > maxTokens) {
181251
+ chunks.push(current);
181252
+ current = line;
181253
+ continue;
181254
+ }
181255
+ current = candidate;
181256
+ }
181257
+ if (current.length > 0) chunks.push(current);
181258
+ return chunks;
181259
+ }
181115
181260
  var system_prompt_default = "You are a strict Git commit message generator. ONLY output the commit message, nothing else.\n\nREQUIRED FORMAT:\n- First line: <type>(<scope>): <subject> (max 50 characters)\n- Types: feat, fix, docs, style, refactor, perf, test, chore, ci\n- Subject: imperative mood, lowercase, no period\n- If body needed: blank line + detailed explanation (wrapped at 72 chars)\n\nSTRICT RULES:\n1. MUST use conventional commits format: type(scope): subject\n2. Subject line MUST be under 50 characters\n3. Subject MUST be in imperative mood (e.g., \"add\", \"fix\", \"update\")\n4. Subject MUST be lowercase\n5. Subject MUST NOT end with a period\n6. MUST provide clear context about WHAT changed and WHY\n7. MUST be specific about affected functions, components, or modules\n8. MUST NOT use vague terms like \"Update\", \"Fix stuff\", \"Changes\"\n9. If including a body, MUST have exactly one blank line between subject and body\n10. Body lines MUST wrap at 72 characters\n\nOUTPUT INSTRUCTION:\nReturn ONLY the commit message. No explanations, no extra text, no markdown formatting.\n\nEXAMPLE:\nfeat(llm): add support for streaming responses\n\nImplement streaming response handling for commit message\ngeneration to improve user experience with large diffs.\nAdds new stream event handlers and updates the API contract.\n\nAnalyze the diff and generate a message following ALL rules above.";
181116
- async function generateCommitMessage(languageModel, instructions, diff) {
181261
+ const summarizePrompt = `You summarize parts of a large git diff for a commit message generator.
181262
+ Describe WHAT changed, in which files or modules, and any observable intent.
181263
+ Be factual and terse. Do NOT write a commit message. Maximum 120 words.`;
181264
+ async function ask(languageModel, system, messages) {
181117
181265
  return (await generateText({
181118
181266
  model: languageModel,
181119
- instructions: system_prompt_default,
181120
- messages: [{
181121
- role: "user",
181122
- content: `Changes:\n${diff}`
181123
- }, {
181124
- role: "user",
181125
- content: instructions || "Generate a concise git commit message based on the above instructions and diff."
181126
- }]
181267
+ instructions: system,
181268
+ messages
181127
181269
  })).text.trim();
181128
181270
  }
181271
+ async function generateCommitMessage(languageModel, instructions, diff) {
181272
+ return ask(languageModel, system_prompt_default, [{
181273
+ role: "user",
181274
+ content: `Changes:\n${diff}`
181275
+ }, {
181276
+ role: "user",
181277
+ content: instructions || "Generate a concise git commit message based on the above instructions and diff."
181278
+ }]);
181279
+ }
181280
+ async function summarizeChanges(languageModel, toc, chunk$1) {
181281
+ return ask(languageModel, summarizePrompt, [{
181282
+ role: "user",
181283
+ content: `All changed files:\n${toc}\n\nChanges (part of a larger diff):\n${chunk$1}`
181284
+ }, {
181285
+ role: "user",
181286
+ content: "Summarize these changes."
181287
+ }]);
181288
+ }
181289
+ async function generateCommitMessageFromSummaries(languageModel, instructions, toc, summaries) {
181290
+ return ask(languageModel, system_prompt_default, [{
181291
+ role: "user",
181292
+ content: `All changed files:\n${toc}\n\nSummaries of all changes:\n${summaries.map((summary, index$2) => `Part ${index$2 + 1}:\n${summary}`).join("\n\n")}`
181293
+ }, {
181294
+ role: "user",
181295
+ content: instructions || "Generate a concise git commit message covering ALL parts above."
181296
+ }]);
181297
+ }
181129
181298
  function resolveLanguageModel(modelConfig) {
181130
181299
  const envVarNames = Array.isArray(modelConfig.apiKeyEnv) ? modelConfig.apiKeyEnv : [modelConfig.apiKeyEnv];
181131
181300
  let apiKey = null;
@@ -197959,6 +198128,10 @@ async function runWithLoading(label, task) {
197959
198128
  readline.clearLine(process.stdout, 0);
197960
198129
  }
197961
198130
  }
198131
+ const MAP_CONCURRENCY = 3;
198132
+ const PROMPT_RESERVE_TOKENS = 800;
198133
+ const MIN_CHUNK_BUDGET_TOKENS = 1e3;
198134
+ const MAX_TOC_LINES = 500;
197962
198135
  async function mainController(options = {}) {
197963
198136
  const { git, liveGit } = await getGit();
197964
198137
  const config$4 = await loadConfig((await git.revparse(["--show-toplevel"])).trim());
@@ -197992,7 +198165,15 @@ async function mainController(options = {}) {
197992
198165
  }
197993
198166
  if (finalCommitMessage.length === 0) while (true) {
197994
198167
  if (forceLLMGenerate) console.log(source_default.yellow("• Using LLM to generate message"));
197995
- finalCommitMessage = (await runWithLoading("Generating commit message", () => generateCommitMessage(languageModel, config$4.instructions ?? null, diff))).trim();
198168
+ finalCommitMessage = (await runWithLoading("Generating commit message", () => generateMessage({
198169
+ git,
198170
+ languageModel,
198171
+ instructions: config$4.instructions ?? null,
198172
+ diff,
198173
+ files,
198174
+ maxDiffTokens: config$4.maxDiffTokens ?? DEFAULT_MAX_DIFF_TOKENS,
198175
+ perFileCap: config$4.perFileCap ?? DEFAULT_PER_FILE_CAP
198176
+ }))).trim();
197996
198177
  if (finalCommitMessage.length === 0) throw new Error("The selected model returned an empty commit message.");
197997
198178
  console.log(source_default.cyan.dim(finalCommitMessage));
197998
198179
  console.log("");
@@ -198012,6 +198193,34 @@ async function mainController(options = {}) {
198012
198193
  await liveGit.push();
198013
198194
  if (config$4.postCommand === "push-and-pull") await liveGit.pull();
198014
198195
  }
198196
+ async function generateMessage(options) {
198197
+ const { git, languageModel, instructions, diff } = options;
198198
+ if (estimateTokens(diff) <= options.maxDiffTokens) return generateCommitMessage(languageModel, instructions, diff);
198199
+ console.log(source_default.yellow("• Large diff detected, minimizing"));
198200
+ const { toc, body } = await minimizeDiff(git, {
198201
+ perFileCap: options.perFileCap,
198202
+ allFiles: options.files
198203
+ });
198204
+ if (estimateTokens(toc) + estimateTokens(body) <= options.maxDiffTokens) return generateCommitMessage(languageModel, instructions, `${toc}\n\n${body}`);
198205
+ console.log(source_default.yellow("• Diff still too large, summarizing in parts"));
198206
+ let effectiveToc = toc;
198207
+ let chunkBudget = options.maxDiffTokens - estimateTokens(effectiveToc) - PROMPT_RESERVE_TOKENS;
198208
+ if (chunkBudget < MIN_CHUNK_BUDGET_TOKENS) {
198209
+ effectiveToc = toc.split("\n").slice(0, MAX_TOC_LINES).join("\n");
198210
+ chunkBudget = Math.max(options.maxDiffTokens - estimateTokens(effectiveToc) - PROMPT_RESERVE_TOKENS, MIN_CHUNK_BUDGET_TOKENS);
198211
+ }
198212
+ const chunks = splitDiffIntoChunks(body, chunkBudget);
198213
+ const summaries = new Array(chunks.length);
198214
+ let nextChunk = 0;
198215
+ async function worker() {
198216
+ while (nextChunk < chunks.length) {
198217
+ const index$2 = nextChunk++;
198218
+ summaries[index$2] = await summarizeChanges(languageModel, effectiveToc, chunks[index$2]);
198219
+ }
198220
+ }
198221
+ await Promise.all(Array.from({ length: Math.min(MAP_CONCURRENCY, chunks.length) }, worker));
198222
+ return generateCommitMessageFromSummaries(languageModel, instructions, effectiveToc, summaries);
198223
+ }
198015
198224
  var AppUserCanceledError = class extends Error {};
198016
198225
  function handleError(fn$1) {
198017
198226
  Promise.resolve().then(fn$1).catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,10 @@
1
1
  {
2
2
  "name": "gityo",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/NazmusSayad/gityo.git"
7
+ },
4
8
  "sideEffects": false,
5
9
  "type": "module",
6
10
  "scripts": {
@@ -13,7 +17,7 @@
13
17
  "json": "tsx ./src/json.ts"
14
18
  },
15
19
  "bin": {
16
- "gityo": "./dist/index.js"
20
+ "gityo": "dist/index.js"
17
21
  },
18
22
  "devDependencies": {
19
23
  "@ai-sdk/amazon-bedrock": "^5.0.40",