1
0

Revert README for consistency

This commit is contained in:
2025-05-24 19:19:22 +02:00
parent 5333b08b4d
commit 06776628f1
3 changed files with 259 additions and 127 deletions

13
editor-reword.js Normal file
View File

@@ -0,0 +1,13 @@
// editor-reword.js
const fs = require('fs');
const map = JSON.parse(process.env.REBASE_COMMIT_MAP);
const msgFile = process.argv[2];
const origMsg = fs.readFileSync(msgFile, 'utf-8');
// Hash suchen
const hashMatch = origMsg.match(/commit\\s+([a-f0-9]{7,40})/i);
const hash = hashMatch ? hashMatch[1] : null;
if (hash && map[hash]) {
fs.writeFileSync(msgFile, map[hash] + '\n');
}
process.exit(0);

371
main.js
View File

@@ -2,8 +2,10 @@ const { app, BrowserWindow, ipcMain, dialog, Menu, shell, clipboard } = require(
app.name = 'Auto-Git'; app.name = 'Auto-Git';
const { exec } = require('child_process'); const { exec } = require('child_process');
const { spawn } = require('child_process'); const { spawn } = require('child_process');
const { spawnSync } = require('child_process');
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const os = require('os');
const Store = require('electron-store'); const Store = require('electron-store');
const simpleGit = require('simple-git'); const simpleGit = require('simple-git');
//const fetch = require('node-fetch'); //const fetch = require('node-fetch');
@@ -19,6 +21,16 @@ const store = new Store({
} }
}); });
let folders = store.get('folders');
if (Array.isArray(folders)) {
folders = folders.map(f => ({
...f,
linesChanged: 0, // zurück auf 0
llmCandidates: [] // leeres Array
}));
store.set('folders', folders);
}
// Map zum Speichern der Watcher pro Ordner // Map zum Speichern der Watcher pro Ordner
const repoWatchers = new Map(); const repoWatchers = new Map();
@@ -133,6 +145,9 @@ function startMonitoringWatcher(folderPath, win) {
// Initialer Commit // Initialer Commit
(async () => { (async () => {
debug(`[MONITOR] Starte initialen Commit-Check für ${folderPath}`); debug(`[MONITOR] Starte initialen Commit-Check für ${folderPath}`);
const git = simpleGit(folderPath); const git = simpleGit(folderPath);
const status = await git.status(); const status = await git.status();
if ( if (
@@ -185,65 +200,46 @@ function stopMonitoringWatcher(folderPath) {
// ---- Rewrite Git Messages with LLM generated messages ---- // ---- Rewrite Git Messages with LLM generated messages ----
// ---- 1. Commits & Diffs für LLM sammeln ----
// 1. Commits & Diffs für LLM sammeln
async function getCommitsForLLM(folderPath, hashes) { async function getCommitsForLLM(folderPath, hashes) {
const git = simpleGit(folderPath); const git = simpleGit(folderPath);
const commits = []; const commits = [];
for (const hash of hashes) { for (const hash of hashes) {
const diff = await git.diff([`${hash}^!`]); const diff = await git.diff([`${hash}^!`]);
const msg = (await git.show(['-s', '--format=%B', hash])).trim(); const msg = (await git.show(['-s', '--format=%B', hash])).trim();
commits.push({ commits.push({ hash, message: msg, diff });
hash,
message: msg,
diff
});
} }
return commits; return commits;
} }
async function getPrompt(folderPath, hashes) { // ---- 2. Prompt für LLM bauen ----
async function generateLLMCommitMessages(folderPath, hashes) {
const commits = await getCommitsForLLM(folderPath, hashes); const commits = await getCommitsForLLM(folderPath, hashes);
const prompt = `
Analyze the following git commits. For each commit, generate a concise commit message summarizing the actual change.
- ONLY output a JSON object mapping each commit hash to its new message.
- Do NOT add any explanations, greetings, or extra text.
if (commits.length === 1) { Example Output:
// Only one commit: Prompt LLM for a message just for this diff. {
const diff = commits[0].diff; "1a2b3c4": "Fix bug in user registration",
return `You're a professional programmer who writes git commit messages all day. "2b3c4d5": "Refactor login logic"
Generate a concise git commit message for these changes:
${diff}
--------------------------------------
Answer VERY BRIEFLY. Don't give any feedback on the code, just analyze what changed and write the git commit message. Keep it short! A commit message MUST BE STRAIGHT TO THE POINT!
Also reply to my message, just give me the commit message.`;
} else if (commits.length > 1) {
// Multiple commits: Squash them, give all diffs as a big change.
const combinedDiffs = commits.map(c => c.diff).join('\n\n');
return `You're a professional programmer who writes git commit messages all day.
Analyze the following code changes (from multiple commits). To squash them into a single commit I need a concise git commit message from you describing the changes in a single sentence.
Here are the combined diffs:
${combinedDiffs}
--------------------------------------
Even if this might seem like a lot of code, I need you to answer VERY BRIEFLY. A git commit message to be precise is what I need from you, to protocol these changes.
Don't give any feedback on the code! Just analyze what changed and write the git commit message. Keep it short! A commit message MUST BE STRAIGHT TO THE POINT!
Don't reply to my message, just give me the commit message.`;
} else {
throw new Error('No commits found for LLM prompt.');
}
} }
COMMITS (as JSON):
// 3. LLM Streaming Call ${JSON.stringify(commits, null, 2)}
`;
return prompt;
}
// ---- 3. LLM Streaming Call ----
async function streamLLMCommitMessages(prompt, onDataChunk) { async function streamLLMCommitMessages(prompt, onDataChunk) {
console.log(prompt);
const response = await fetch('http://localhost:11434/api/generate', { const response = await fetch('http://localhost:11434/api/generate', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
model: 'qwen2.5-coder:32b', model: 'qwen2.5-coder:32b', // ggf. Modell anpassen
prompt: prompt, prompt: prompt,
stream: true stream: true
}) })
@@ -278,98 +274,225 @@ async function streamLLMCommitMessages(prompt, onDataChunk) {
return fullOutput; return fullOutput;
} }
function cleanRebaseDirs(repoPath) { // ---- 4. JSON Output robust parsen ----
const gitDir = path.join(repoPath, '.git'); function parseLLMCommitMessages(rawOutput) {
const rebaseDirs = ['rebase-merge', 'rebase-apply']; let cleaned = rawOutput.trim();
for (const dir of rebaseDirs) { cleaned = cleaned.replace(/^```(?:json)?|```$/gmi, '');
const fullPath = path.join(gitDir, dir);
if (fs.existsSync(fullPath)) { try {
fs.rmSync(fullPath, { recursive: true, force: true }); if (cleaned.trim().startsWith('[')) return JSON.parse(cleaned);
console.log(`[AutoGit] Entfernt alte ${dir}-Direktory: ${fullPath}`); if (cleaned.trim().startsWith('{')) {
const obj = JSON.parse(cleaned);
return Object.entries(obj).map(([commit, newMessage]) => ({ commit, newMessage }));
}
// Zeilenweise Objekte (fuzzy)
if (cleaned.includes('\n')) {
let lines = cleaned.split('\n').map(l => l.trim()).filter(Boolean);
let objs = [];
for (let line of lines) {
try { let o = JSON.parse(line); objs.push(o); } catch {}
}
if (objs.length) return objs;
}
} catch (err) {
// Fallback repair
try {
cleaned = cleaned.replace(/}\s*{/g, '},\n{');
if (!cleaned.startsWith('[')) cleaned = '[' + cleaned;
if (!cleaned.endsWith(']')) cleaned = cleaned + ']';
return JSON.parse(cleaned);
} catch (e) {
throw new Error('Could not parse LLM output:\n' + rawOutput);
} }
} }
throw new Error('Could not parse LLM output:\n' + rawOutput);
} }
const fs = require('fs');
const path = require('path');
const os = require('os');
const { spawn } = require('child_process');
const simpleGit = require('simple-git');
function cleanRebaseDirs(repoPath) {
const dirs = ['rebase-merge', 'rebase-apply'];
for (const d of dirs) {
const p = path.join(repoPath, '.git', d);
if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
}
}
async function squashCommitMessages(repoPath, commitMessage, hashes) {
cleanRebaseDirs(repoPath); // --- Hauptfunktion ---
/**
* Rewords commit messages for each hash (oldest to newest) using git rebase -i in a loop.
* @param {string} repoPath - Path to your repo
* @param {object} commitMessageMap - { fullHash: newMessage }
* @param {string[]} hashes - array of full commit hashes (oldest first!)
*/
async function rewordCommitsSequentially(repoPath, commitMessageMap, hashes) {
const git = simpleGit(repoPath); const git = simpleGit(repoPath);
// 1. Find the oldest commit in the sequence // Sanity: Sort hashes chronologically by git log order (oldest first)
const allCommits = (await git.log()).all; const allCommits = (await git.log()).all;
const hashIdxs = hashes.map(h => allCommits.findIndex(c => c.hash.startsWith(h))); const hashesOrdered = hashes
if (hashIdxs.includes(-1)) throw new Error('Some commit(s) not found.'); .map(h => allCommits.find(c => c.hash.startsWith(h)))
const oldestIdx = Math.min(...hashIdxs); .filter(Boolean)
.sort((a, b) =>
allCommits.findIndex(c => c.hash === a.hash) - allCommits.findIndex(c => c.hash === b.hash)
)
.map(c => c.hash);
// The commit *before* the oldest in the list is the "upstream" for rebase // Loop over all hashes
const rebaseFrom = oldestIdx + 1 < allCommits.length ? allCommits[oldestIdx + 1].hash : '--root'; for (const hash of hashesOrdered) {
// Compose the rebase command: only one commit at a time!
await new Promise((resolve, reject) => {
// macOS: sed -i '' ... Linux: sed -i ...
// Try macOS style, change '' to '' or nothing if you get errors.
const sequenceEditor = `sed -i '' '1s/pick/reword/'`;
const commitMsg = commitMessageMap[hash].replace(/(["$`\\])/g, '\\$1'); // Escape quotes etc
// 2. Collect all commits to squash (in reverse = chronological) const proc = spawn('git', [
const toSquash = allCommits.slice(0, oldestIdx + 1).reverse(); 'rebase', '-i', `${hash}^`
if (toSquash.length < 2) throw new Error("Need at least 2 commits to squash."); ], {
cwd: repoPath,
const first = toSquash[0]; env: {
const rest = toSquash.slice(1); ...process.env,
GIT_SEQUENCE_EDITOR: sequenceEditor,
const todoLines = [ GIT_EDITOR: `echo "${commitMsg}" >`
`pick ${first.hash} ${first.message.split('\n')[0]}`, },
...rest.map(c => `squash ${c.hash} ${c.message.split('\n')[0]}`) stdio: 'inherit'
]; });
proc.on('exit', code => code === 0 ? resolve() : reject(new Error(`Failed to reword ${hash}`)));
// Write the todo file
const tmpdir = os.tmpdir();
const todoPath = path.join(tmpdir, `git-rebase-todo-${Date.now()}.txt`);
fs.writeFileSync(todoPath, todoLines.join('\n'));
// Create a temporary script for SEQUENCE_EDITOR
const seqScript = path.join(tmpdir, `llm-seq-edit-${Date.now()}.sh`);
fs.writeFileSync(seqScript, `#!/bin/sh\ncp "${todoPath}" "$1"\n`);
fs.chmodSync(seqScript, 0o755);
// Create a temporary script for GIT_EDITOR (sets commit message)
const msgScript = path.join(tmpdir, `llm-msg-edit-${Date.now()}.sh`);
fs.writeFileSync(msgScript, `#!/bin/sh\necho "${commitMessage.replace(/"/g, '\\"')}" > "$1"\n`);
fs.chmodSync(msgScript, 0o755);
// Launch rebase
await new Promise((resolve, reject) => {
const proc = spawn('git', ['rebase', '-i', rebaseFrom], {
cwd: repoPath,
env: {
...process.env,
GIT_SEQUENCE_EDITOR: seqScript,
GIT_EDITOR: msgScript
},
stdio: 'inherit'
}); });
proc.on('exit', code => { console.log(`[AutoGit] Reworded commit ${hash}`);
try { }
fs.unlinkSync(todoPath); console.log('[AutoGit] All specified commit messages updated!');
fs.unlinkSync(seqScript);
fs.unlinkSync(msgScript);
} catch {}
if (code === 0) resolve();
else reject(new Error('git rebase failed with exit code ' + code));
});
});
} }
/** /**
* Komplett-Workflow: Von Kandidaten bis Rewrite * Cherry-picks the given commits, amending each one's message, and replaces master.
* @param {string} repoPath
* @param {object} commitMessageMap - { [fullHash]: newMessage }
* @param {string[]} hashes - list of commit hashes, oldest to newest!
*/ */
/*
async function cherryPickCommitRewrite(repoPath, commitMessageMap, hashes) {
// 1. Find parent of the OLDEST commit
//const allCommits = (await git.log()).all;
//const parentHash = (await git.raw(['rev-parse', `${oldestHash}^`])).trim();
// 2. Create a new temp branch from the parent
const git = simpleGit(repoPath);
if(hashes.length > 1){
const branchName = "temp_branch" + Date.now();
console.log(commitMessageMap[hashes[0]]);
await git.checkout(hashes[0]);
console.log("checkout " + hashes[0])
await git.checkoutLocalBranch(branchName);
console.log("branch " + branchName)
await git.commit(commitMessageMap[hashes[0]], undefined, { '--amend': null });
console.log("amend")
for (let i = 1; i < hashes.length; i++) {
await git.raw(['cherry-pick', '--no-commit', hashes[i]]);
console.log("cherry " + hashes[i])
await git.commit(commitMessageMap[hashes[i]], undefined, { '--amend': null });
console.log("amend")
}
await git.deleteLocalBranch('master', true);
console.log("branch del")
await git.branch(['-m', branchName, 'master']);
console.log("branch mov")
} else {
await git.commit(commitMessageMap[hashes[0]], undefined, { '--amend': null });
}
*/
//await git.checkoutLocalBranch(NEW_BRANCH);
/*
// 3. Cherry-pick and amend each commit in order
for (const hash of hashes) {
// Cherry-pick (commit as is)
let res = spawnSync('git', ['cherry-pick', hash], { cwd: repoPath, stdio: 'inherit' });
if (res.status !== 0) throw new Error('Cherry-pick failed for ' + hash);
// Amend commit message
const msg = commitMessageMap[hash];
if (msg) {
res = spawnSync('git', ['commit', '--amend', '-m', msg], { cwd: repoPath, stdio: 'inherit' });
if (res.status !== 0) throw new Error('Amend failed for ' + hash);
}
}
// 4. Move master to rewritten branch (overwrite)
await git.checkout('master'); // just in case we're not already there
await git.branch(['-f', 'master', NEW_BRANCH]); // force-move master pointer
// 5. Checkout master (HEAD on new history)
await git.checkout('master');
// 6. (Optional) Delete the temp branch
await git.branch(['-D', NEW_BRANCH]);
console.log('\n[AutoGit] Master branch has been overwritten with rewritten commits.');
*/
//}
// ---- 6. Komplett-Workflow: Von Kandidaten bis Rewrite ----
/*
async function runLLMCommitRewrite(folderPath, hashes) {
const prompt = await generateLLMCommitMessages(folderPath, hashes);
const llmRaw = await streamLLMCommitMessages(prompt, chunk => process.stdout.write(chunk));
const commitList = parseLLMCommitMessages(llmRaw);
const messageMap = {};
for (const entry of commitList) messageMap[entry.commit] = entry.newMessage;
await cherryPickCommitRewrite(folderPath, messageMap, hashes);
}
*/
// ---- 6. Komplett-Workflow (Randomized) ----
async function runLLMCommitRewrite(folderPath, hashes) {
// Generate a mapping { hash: message }
const messageMap = hashes.reduce((map, hash) => {
map[hash] = getRandomMessage();
return map;
}, {});
console.log(messageMap)
// Call your existing rewrite step with the fake messages
//await cherryPickCommitRewrite(folderPath, messageMap, hashes);
await rewordCommitsSequentially(folderPath, messageMap, hashes);
}
// Helper: returns a “random” placeholder commit message
function getRandomMessage() {
const verbs = [
'Update', 'Refactor', 'Fix', 'Add', 'Remove', 'Improve', 'Optimize',
'Document', 'Cleanup', 'Configure', 'Upgrade', 'Revert'
];
const objects = [
'authentication flow', 'API endpoint', 'styling', 'logging',
'error handling', 'data model', 'build script', 'test suite',
'configuration', 'dependencies', 'README', 'README.md'
];
const details = [
'for better performance',
'to meet new requirements',
'after feedback',
'as per spec',
'to fix typo',
'to improve readability',
'to avoid regressions',
'for consistency'
];
const pick = arr => arr[Math.floor(Math.random() * arr.length)];
return `${pick(verbs)} ${pick(objects)} ${pick(details)}`;
}
// Nutze das Template aus dem Projektordner:
const TEMPLATE_PATH = path.join(__dirname, 'rewrite-commit-msg.js.template');
function createRewriteScript(mapping) {
// Lies das Template
let content = fs.readFileSync(TEMPLATE_PATH, 'utf-8');
// Ersetze __MESSAGE_MAP__ durch dein Mapping
content = content.replace('__MESSAGE_MAP__', JSON.stringify(mapping));
// Speichere als temporäre Datei
const scriptPath = path.join(__dirname, `rewrite-commit-msg.${Date.now()}.js`);
fs.writeFileSync(scriptPath, content, 'utf-8');
return scriptPath;
}
async function autoCommit(folderPath, message) { async function autoCommit(folderPath, message) {
@@ -470,22 +593,18 @@ async function autoCommit(folderPath, message) {
const newHead = (await git.revparse(['HEAD'])).trim(); const newHead = (await git.revparse(['HEAD'])).trim();
folders[idx].llmCandidates = folders[idx].llmCandidates || []; folders[idx].llmCandidates = folders[idx].llmCandidates || [];
folders[idx].llmCandidates.push(newHead); folders[idx].llmCandidates.push(newHead);
console.log(folders[idx].llmCandidates)
// Threshold holen // Threshold holen
const threshold = store.get('intelligentCommitThreshold') || 150; const threshold = store.get('intelligentCommitThreshold') || 10;
if (folders[idx].linesChanged >= threshold) { if (folders[idx].linesChanged >= threshold) {
debug('Congratulations! You changed enough lines of code :)'); debug('Congratulations! You changed enough lines of code :)');
// **Hier: LLM-Workflow starten**
//await runLLMCommitPipeline(folderPath, folders[idx].llmCandidates, win);
folders[idx].linesChanged = 0; folders[idx].linesChanged = 0;
const candidates = folders[idx].llmCandidates; const cands = folders[idx].llmCandidates;
folders[idx].llmCandidates = []; folders[idx].llmCandidates = [];
await runLLMCommitPipeline(folderPath, candidates); await runLLMCommitRewrite(folderPath, cands);
// Reset danach: store.set('folders', folders);
//store.set('folders', folders);
} }
// Folder-Objekt speichern
store.set('folders', folders); store.set('folders', folders);
} else { } else {
// Folder not found! (Debug) // Folder not found! (Debug)
@@ -854,7 +973,7 @@ app.whenReady().then(() => {
); );
store.set('folders', folders); store.set('folders', folders);
debug(`[STORE] Monitoring für ${folderPath}: ${monitoring}`); debug(`[STORE] Monitoring für ${folderPath}: ${monitoring}`);
// Monitoring-Watcher starten/stoppen → gleich mehr dazu // Monitoring-Watcher starten/stoppen
if (monitoring) { if (monitoring) {
startMonitoringWatcher(folderPath, win); startMonitoringWatcher(folderPath, win);
} else { } else {

View File

@@ -35,4 +35,4 @@ ipcRenderer.on('repo-updated', (_e, folder) => {
ipcRenderer.on('skymode-changed', (_e, val) => { ipcRenderer.on('skymode-changed', (_e, val) => {
window.dispatchEvent(new CustomEvent('skymode-changed', { detail: val })); window.dispatchEvent(new CustomEvent('skymode-changed', { detail: val }));
}); });