1
0

auto-git:

[add] scripts/
 [add] src-tauri/
 [add] tauriBridge.js
 [change] .gitignore
 [change] README.md
 [change] index.html
 [change] package.json
 [change] renderer.js
 [change] settings.html
 [unlink] editor-reword.js
 [unlink] main.js
 [unlink] preload.js
 [unlink] rebase-sequence-windows.js
This commit is contained in:
2026-05-04 21:33:17 +02:00
parent 82d9c45bda
commit 0e12ac604e
18 changed files with 9782 additions and 3559 deletions

4
.gitignore vendored
View File

@@ -1,5 +1,8 @@
node_modules
package-lock.json
dist-tauri
src-tauri/target
src-tauri/gen
dist.DS_Store
.git
.gitignore
@@ -7,6 +10,7 @@ dist.DS_Store
*.iml
yarn.lock
*.lock
!src-tauri/Cargo.lock
Makefile
*.exe
tsconfig.tsbuildinfo

View File

@@ -99,38 +99,23 @@ Download the latest release for your platform:
If you want to build Auto-Git yourself, follow these steps:
1. Clone or download the repository to your local machine.
2. Install Node.js (version 16+ recommended) and npm.
2. Install Node.js, npm, Rust, and the Tauri prerequisites for your operating system.
3. Open a terminal, navigate into the project folder, and run:
```bash
npm install
```
4. Optional: If you need to adjust architectures or targets, modify `package.json` under the `"build"` section.
- Example for Windows x64 only:
```json
"build": {
"win": {
"icon": "win/icon.ico",
"target": [
{
"target": "nsis",
"arch": ["x64"]
}
]
}
}
```
5. Build the distributables:
4. Run the app in development mode:
```bash
npm start
```
5. Build the Tauri distributables:
```bash
npm run dist
```
- On an ARM64 machine, to produce an x64 Windows installer, first ensure `"arch": ["x64"]` is under `"win.target"`, then:
```bash
npm run dist
```
6. The output installers/packages will be located in the `dist/` directory.
6. The output installers/packages will be located under `src-tauri/target/release/bundle/`.
---
## License
This project is licensed under the MIT License. See [LICENSE](LICENSE) for details.
This project is licensed under the MIT License. See [LICENSE](LICENSE) for details.

View File

@@ -1,13 +0,0 @@
// 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);

View File

@@ -4,6 +4,7 @@
<meta charset="UTF-8" />
<title>auto-git</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="tauriBridge.js"></script>
<style>
:root {
/* Default-Modus: Weißes Panel mit Rosa-Akzenten */

3356
main.js

File diff suppressed because it is too large Load Diff

View File

@@ -4,52 +4,15 @@
"version": "1.0.0",
"description": "Auto-Git: Git-Überwachung mit automatischer LLM-Commit-Message und README-Erstellung",
"author": "Victor Giers",
"main": "main.js",
"scripts": {
"start": "electron .",
"dist": "electron-builder --publish never"
},
"dependencies": {
"chokidar": "^4.0.3",
"electron-store": "^8.2.0",
"ignore": "^7.0.4",
"micromatch": "^4.0.8",
"node-fetch": "^3.3.2",
"simple-git": "^3.20.0",
"suncalc": "^1.9.0"
"prepare:tauri-frontend": "node scripts/prepare-tauri-frontend.mjs",
"start": "npm run prepare:tauri-frontend && tauri dev",
"tauri:dev": "npm run prepare:tauri-frontend && tauri dev",
"dist": "npm run prepare:tauri-frontend && tauri build"
},
"dependencies": {},
"devDependencies": {
"autoprefixer": "^10.4.21",
"electron": "^25.0.0",
"electron-builder": "^26.0.12",
"postcss": "^8.5.3",
"tailwindcss": "^4.1.7"
},
"build": {
"appId": "com.victorgiers.auto-git",
"productName": "Auto-Git",
"files": [
"main.js",
"preload.js",
"renderer.js",
"index.html",
"settings.html",
"animeCat.js",
"rewrite-commit-msg.js.template",
"assets/**/*",
"node_modules/**/*"
],
"directories": {
"buildResources": "assets/icon"
},
"mac": {
"icon": "mac/icon.icns"
},
"win": {
"icon": "win/icon.ico"
},
"linux": {
"icon": "linux/icon.png"
}
"@tauri-apps/api": "^2.11.0",
"@tauri-apps/cli": "^2.11.0"
}
}
}

View File

@@ -1,88 +0,0 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('settingsAPI', {
getTheme: () => ipcRenderer.invoke('get-theme'),
setTheme: val => ipcRenderer.invoke('set-theme', val),
// Legacy helpers: keep compatibility with old sky mode calls
getSkyMode: () => ipcRenderer.invoke('get-theme').then(t => t === 'sky'),
setSkyMode: val => ipcRenderer.invoke('set-theme', val ? 'sky' : 'default'),
getSkipPrompt: () => ipcRenderer.invoke('get-skip-git-prompt'),
setSkipPrompt: val => ipcRenderer.invoke('set-skip-git-prompt', val),
getIntelligentCommitThreshold: () => ipcRenderer.invoke('get-intelligent-commit-threshold'),
setIntelligentCommitThreshold: value => ipcRenderer.invoke('set-intelligent-commit-threshold', value),
getMinutesCommitThreshold: () => ipcRenderer.invoke('get-minutes-commit-threshold'),
setMinutesCommitThreshold: value => ipcRenderer.invoke('set-minutes-commit-threshold', value),
getCommitModel: () => ipcRenderer.invoke('get-commit-model'),
setCommitModel: (model) => ipcRenderer.invoke('set-commit-model', model),
getReadmeModel: () => ipcRenderer.invoke('get-readme-model'),
setReadmeModel: (model) => ipcRenderer.invoke('set-readme-model', model),
close: () => ipcRenderer.send('close-settings'),
getAutostart: () => ipcRenderer.invoke('get-autostart'),
setAutostart: val => ipcRenderer.invoke('set-autostart', val),
getCloseToTray: () => ipcRenderer.invoke('get-close-to-tray'),
setCloseToTray: val => ipcRenderer.invoke('set-close-to-tray', val),
getGiteaToken: () => ipcRenderer.invoke('get-gitea-token'),
setGiteaToken: (token) => ipcRenderer.invoke('set-gitea-token', token),
});
contextBridge.exposeInMainWorld('electronAPI', {
getFolders: () => ipcRenderer.invoke('get-folders'),
addFolder: () => ipcRenderer.invoke('add-folder'),
removeFolder: folderObj=> ipcRenderer.invoke('remove-folder', folderObj),
getSelected: () => ipcRenderer.invoke('get-selected'),
setSelected: folderObj=> ipcRenderer.invoke('set-selected', folderObj),
getCommits: (folderObj, page, pageSize) => ipcRenderer.invoke('get-commits', folderObj, page, pageSize),
getAllCommitHashes: (folderObj) => ipcRenderer.invoke('get-all-commit-hashes', folderObj),
diffCommit: (folderObj, hash) => ipcRenderer.invoke('diff-commit', folderObj, hash),
revertCommit: (folderObj, hash) => ipcRenderer.invoke('revert-commit', folderObj, hash),
snapshotCommit: (folderObj, hash) => ipcRenderer.invoke('snapshot-commit', folderObj, hash),
checkoutCommit: (folderObj, hash) => ipcRenderer.invoke('checkout-commit', folderObj, hash),
getCommitCount: folderObj=> ipcRenderer.invoke('get-commit-count', folderObj),
hasDiffs: folderObj=> ipcRenderer.invoke('has-diffs', folderObj),
removeGitFolder:folderObj=> ipcRenderer.invoke('remove-git-folder', folderObj),
commitCurrentFolder: (folderObj, message) => ipcRenderer.invoke('commit-current-folder', folderObj, message),
showFolderContextMenu: folderPath => ipcRenderer.send('show-folder-context-menu', folderPath),
setMonitoring: (folderObj, monitoring) => ipcRenderer.invoke('set-monitoring', folderObj.path, monitoring),
getFolderTree: (folderPath) => ipcRenderer.invoke('get-folder-tree', folderPath),
showTreeContextMenu: (info) => ipcRenderer.send('show-tree-context-menu', info),
getSelectedSync: () => ipcRenderer.sendSync('get-selected-sync'),
getIntelligentCommitThreshold: () => ipcRenderer.invoke('get-intelligent-commit-threshold'),
setIntelligentCommitThreshold: value => ipcRenderer.invoke('set-minutes-commit-threshold', value),
getMinutesCommitThreshold: () => ipcRenderer.invoke('get-minutes-commit-threshold'),
setMinutesCommitThreshold: value => ipcRenderer.invoke('set-intelligent-commit-threshold', value),
ollamaList: () => ipcRenderer.invoke('ollama-list'),
ollamaPull: (model) => ipcRenderer.invoke('ollama-pull', model),
onTrayToggleMonitoring: (callback) => ipcRenderer.on('tray-toggle-monitoring', callback),
onTrayRemoveFolder: (callback) => ipcRenderer.on('tray-remove-folder', callback),
onTrayAddFolder: (callback) => ipcRenderer.on('tray-add-folder', callback),
onFoldersLocationUpdated: (callback) => ipcRenderer.on('folders-location-updated', (e, folderObj) => callback(folderObj)),
isGitRepo: (path) => ipcRenderer.invoke('is-git-repo', path),
relocateFolder: (oldPath, newPath) => ipcRenderer.invoke('relocate-folder', oldPath, newPath),
pickFolder: () => ipcRenderer.invoke('pick-folder'),
repoHasCommit: (repoPath, commitHash) => ipcRenderer.invoke('repo-has-commit', repoPath, commitHash),
addFolderByPath: (folderPath) => ipcRenderer.invoke('add-folder-by-path', folderPath),
onCatBegin: (cb) => ipcRenderer.on('cat-begin', cb),
onCatChunk: (cb) => ipcRenderer.on('cat-chunk', cb),
onCatEnd: (cb) => ipcRenderer.on('cat-end', cb),
getDailyCommitStats: () => ipcRenderer.invoke('get-daily-commit-stats'),
hasReadme: (folderPath) => ipcRenderer.invoke('has-readme', folderPath),
generateReadme: (folderPath) => ipcRenderer.invoke('generate-readme', folderPath),
squashCommits: (folderPath) => ipcRenderer.invoke('squash-commits', folderPath),
pushToGitea: (folderPath) => ipcRenderer.invoke('push-to-gitea', folderPath),
initRepo: (folderPath) => ipcRenderer.invoke('init-repo', folderPath),
triggerRewriteNow: (folderPath) => ipcRenderer.invoke('trigger-rewrite-now', folderPath),
});
ipcRenderer.on('repo-updated', (_e, folder) => {
window.dispatchEvent(new CustomEvent('repo-updated', { detail: folder }));
});
ipcRenderer.on('theme-changed', (_e, theme) => {
window.dispatchEvent(new CustomEvent('theme-changed', { detail: theme }));
window.dispatchEvent(new CustomEvent('skymode-changed', { detail: theme === 'sky' }));
});
ipcRenderer.on('skymode-changed', (_e, val) => {
const theme = val ? 'sky' : 'default';
window.dispatchEvent(new CustomEvent('theme-changed', { detail: theme }));
window.dispatchEvent(new CustomEvent('skymode-changed', { detail: val }));
});

View File

@@ -1,34 +0,0 @@
#!/usr/bin/env node
// ─────────────────────────────────────────────────
// rebase-sequence-windows.js
// Dieses Skript wird von Git (als GIT_SEQUENCE_EDITOR) aufgerufen.
// Git übergibt die "todo"-Datei als erstes Argument (process.argv[2]).
// Wir ersetzen dort "pick ..." in der ersten Zeile durch "reword ...".
// ─────────────────────────────────────────────────
const fs = require('fs');
// Git ruft den Editor so auf:
// node rebase-sequence-windows.js <pfad-zur-todo-Datei>
// In process.argv ist:
// [0] = Pfad zur NodeExecutable
// [1] = Pfad zu diesem Skript
// [2] = Pfad zur temporären Todo-Datei
const todoFile = process.argv[2];
if (!todoFile) {
console.error('Usage: rebase-sequence-windows.js <todoFile>');
process.exit(1);
}
try {
const content = fs.readFileSync(todoFile, 'utf8').split(/\r?\n/);
if (content.length > 0 && content[0].startsWith('pick ')) {
content[0] = content[0].replace(/^pick /, 'reword ');
}
fs.writeFileSync(todoFile, content.join('\n'), 'utf8');
} catch (err) {
console.error('Fehler beim Bearbeiten der Rebase-TODO-Datei:', err);
process.exit(1);
}
process.exit(0);

View File

@@ -148,6 +148,17 @@ window.addEventListener('DOMContentLoaded', async () => {
}
}
});
window.addEventListener('tauri-folder-drop', async e => {
const paths = Array.isArray(e.detail) ? e.detail : [];
for (const folderPath of paths) {
await window.electronAPI.addFolderByPath(folderPath);
}
if (paths.length) {
await renderSidebar();
const sel = await window.electronAPI.getSelected();
if (sel) await renderContent(sel);
}
});
// Theme Setup (Sky = dynamic, Default/Grey = static)
const DAY_COLOR = [173, 216, 230];
@@ -530,6 +541,7 @@ window.addEventListener('DOMContentLoaded', async () => {
async function renderContent(folderObj, page) {
closeDropdown();
window.currentSelectedFolderObj = folderObj;
const folder = folderObj.path;
await updateInteractionBar(folderObj);
titleEl.textContent = folder;

View File

@@ -0,0 +1,14 @@
import { cp, mkdir, rm } from 'node:fs/promises';
import path from 'node:path';
const root = process.cwd();
const out = path.join(root, 'dist-tauri');
await rm(out, { recursive: true, force: true });
await mkdir(out, { recursive: true });
for (const file of ['index.html', 'settings.html', 'renderer.js', 'animeCat.js', 'tauriBridge.js']) {
await cp(path.join(root, file), path.join(out, file));
}
await cp(path.join(root, 'assets'), path.join(out, 'assets'), { recursive: true });

View File

@@ -161,6 +161,7 @@
vertical-align: middle;
}
</style>
<script src="tauriBridge.js"></script>
<script>
window.addEventListener('DOMContentLoaded', async () => {
const applyThemeClass = (theme) => {

6016
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

31
src-tauri/Cargo.toml Normal file
View File

@@ -0,0 +1,31 @@
[package]
name = "auto-git"
version = "1.0.0"
description = "Auto-Git: Git monitoring with automatic LLM commit messages and README generation"
authors = ["Victor Giers"]
edition = "2021"
[build-dependencies]
tauri-build = { version = "2.6.0", features = [] }
[dependencies]
arboard = "3.6.1"
base64 = "0.22.1"
dirs = "6.0.0"
globset = "0.4.18"
ignore = "0.4.25"
notify = "8.2.0"
open = "5.3.3"
rfd = "0.15.4"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
tempfile = "3.23.0"
tauri = { version = "2.11.0", features = ["tray-icon", "image-png", "image-ico"] }
thiserror = "2.0.17"
reqwest = { version = "0.12.24", default-features = false, features = ["blocking", "json", "rustls-tls"] }
[target.'cfg(target_os = "macos")'.dependencies]
auto-launch = "0.5.0"
[target.'cfg(not(target_os = "macos"))'.dependencies]
auto-launch = "0.5.0"

3
src-tauri/build.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Default desktop capability for Auto-Git",
"windows": ["main", "settings"],
"permissions": [
"core:default",
"core:event:default",
"core:window:default",
"core:menu:default",
"core:tray:default"
]
}

3506
src-tauri/src/main.rs Normal file

File diff suppressed because it is too large Load Diff

37
src-tauri/tauri.conf.json Normal file
View File

@@ -0,0 +1,37 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Auto-Git",
"version": "1.0.0",
"identifier": "com.victorgiers.auto-git",
"build": {
"frontendDist": "../dist-tauri"
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"label": "main",
"title": "Auto-Git",
"width": 900,
"height": 600,
"minWidth": 800,
"minHeight": 500
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"../assets/icon/linux/icon.png",
"../assets/icon/mac/icon.icns",
"../assets/icon/win/icon.ico"
],
"resources": [
"../assets/**/*"
]
}
}

128
tauriBridge.js Normal file
View File

@@ -0,0 +1,128 @@
(function () {
const tauri = window.__TAURI__;
if (!tauri || window.electronAPI || window.settingsAPI) return;
const invoke = (command, args = {}) => tauri.core.invoke(command, args);
const listen = (event, handler) => tauri.event.listen(event, handler);
let selectedCache = null;
const eventUnsubscribers = [];
function onTauriEvent(eventName, callback, legacyEventArg = true) {
const promise = listen(eventName, event => {
if (legacyEventArg) callback({}, event.payload);
else callback(event.payload);
});
eventUnsubscribers.push(promise);
return promise;
}
async function refreshSelectedCache() {
selectedCache = await invoke('get_selected');
return selectedCache;
}
listen('repo-updated', event => {
window.dispatchEvent(new CustomEvent('repo-updated', { detail: event.payload }));
});
listen('theme-changed', event => {
const theme = event.payload;
window.dispatchEvent(new CustomEvent('theme-changed', { detail: theme }));
window.dispatchEvent(new CustomEvent('skymode-changed', { detail: theme === 'sky' }));
});
listen('skymode-changed', event => {
const val = !!event.payload;
window.dispatchEvent(new CustomEvent('theme-changed', { detail: val ? 'sky' : 'default' }));
window.dispatchEvent(new CustomEvent('skymode-changed', { detail: val }));
});
listen('tauri://drag-drop', event => {
const paths = event.payload?.paths || [];
if (paths.length) {
window.dispatchEvent(new CustomEvent('tauri-folder-drop', { detail: paths }));
}
});
window.settingsAPI = {
getTheme: () => invoke('get_theme'),
setTheme: val => invoke('set_theme', { val }),
getSkyMode: () => invoke('get_theme').then(theme => theme === 'sky'),
setSkyMode: val => invoke('set_theme', { val: val ? 'sky' : 'default' }),
getSkipPrompt: () => invoke('get_skip_git_prompt'),
setSkipPrompt: val => invoke('set_skip_git_prompt', { val }),
getIntelligentCommitThreshold: () => invoke('get_intelligent_commit_threshold'),
setIntelligentCommitThreshold: value => invoke('set_intelligent_commit_threshold', { value }),
getMinutesCommitThreshold: () => invoke('get_minutes_commit_threshold'),
setMinutesCommitThreshold: value => invoke('set_minutes_commit_threshold', { value }),
getCommitModel: () => invoke('get_commit_model'),
setCommitModel: model => invoke('set_commit_model', { val: model }),
getReadmeModel: () => invoke('get_readme_model'),
setReadmeModel: model => invoke('set_readme_model', { val: model }),
close: () => invoke('close_settings'),
getAutostart: () => invoke('get_autostart'),
setAutostart: val => invoke('set_autostart', { enabled: val }),
getCloseToTray: () => invoke('get_close_to_tray'),
setCloseToTray: val => invoke('set_close_to_tray', { val }),
getGiteaToken: () => invoke('get_gitea_token'),
setGiteaToken: token => invoke('set_gitea_token', { token })
};
window.electronAPI = {
getFolders: () => invoke('get_folders'),
addFolder: () => invoke('add_folder'),
removeFolder: folderObj => invoke('remove_folder', { folderObj }),
getSelected: refreshSelectedCache,
setSelected: async folderObj => {
selectedCache = await invoke('set_selected', { folderObjOrPath: folderObj });
return selectedCache;
},
getCommits: (folderObj, page, pageSize) => invoke('get_commits', { folderObj, page, pageSize }),
getAllCommitHashes: folderObj => invoke('get_all_commit_hashes', { folderObj }),
diffCommit: (folderObj, hash) => invoke('diff_commit', { folderObj, hash }),
revertCommit: (folderObj, hash) => invoke('revert_commit', { folderObj, hash }),
snapshotCommit: (folderObj, hash) => invoke('snapshot_commit', { folderObj, hash }),
checkoutCommit: (folderObj, hash) => invoke('checkout_commit', { folderObj, hash }),
getCommitCount: folderObj => invoke('get_commit_count', { folderObj }),
hasDiffs: folderObj => invoke('has_diffs', { folderObj }),
removeGitFolder: folderObj => invoke('remove_git_folder', { folderObj }),
commitCurrentFolder: (folderObj, message) => invoke('commit_current_folder', { folderObj, message }),
showFolderContextMenu: folderPath => invoke('show_folder_context_menu', { folderPath }),
setMonitoring: (folderObj, monitoring) => invoke('set_monitoring', { folderPath: folderObj.path, monitoring }),
getFolderTree: folderPath => invoke('get_folder_tree', { folderPath }),
showTreeContextMenu: info => invoke('show_tree_context_menu', { info }),
getSelectedSync: () => selectedCache || window.currentSelectedFolderObj || null,
getIntelligentCommitThreshold: () => invoke('get_intelligent_commit_threshold'),
setIntelligentCommitThreshold: value => invoke('set_intelligent_commit_threshold', { value }),
getMinutesCommitThreshold: () => invoke('get_minutes_commit_threshold'),
setMinutesCommitThreshold: value => invoke('set_minutes_commit_threshold', { value }),
ollamaList: () => invoke('ollama_list'),
ollamaPull: model => invoke('ollama_pull', { model }),
onTrayToggleMonitoring: callback => onTauriEvent('tray-toggle-monitoring', callback),
onTrayRemoveFolder: callback => onTauriEvent('tray-remove-folder', callback),
onTrayAddFolder: callback => onTauriEvent('tray-add-folder', callback),
onFoldersLocationUpdated: callback => onTauriEvent('folders-location-updated', callback, false),
isGitRepo: path => invoke('is_git_repo', { folderPath: path }),
relocateFolder: (oldPath, newPath) => invoke('relocate_folder', { oldPath, newPath }),
pickFolder: () => invoke('pick_folder'),
repoHasCommit: (repoPath, commitHash) => invoke('repo_has_commit', { repoPath, commitHash }),
addFolderByPath: folderPath => invoke('add_folder_by_path', { folderPath }),
onCatBegin: callback => onTauriEvent('cat-begin', callback),
onCatChunk: callback => onTauriEvent('cat-chunk', callback),
onCatEnd: callback => onTauriEvent('cat-end', callback),
getDailyCommitStats: () => invoke('get_daily_commit_stats'),
hasReadme: folderPath => invoke('has_readme', { folderPath }),
generateReadme: folderPath => invoke('generate_readme', { folderPath }),
squashCommits: folderPath => invoke('squash_commits', { folderPath }),
pushToGitea: folderPath => invoke('push_to_gitea', { folderPath }),
initRepo: folderPath => invoke('init_repo', { folderPath }),
triggerRewriteNow: folderPath => invoke('trigger_rewrite_now', { folderPath })
};
window.addEventListener('beforeunload', () => {
for (const pending of eventUnsubscribers) {
pending.then(unlisten => unlisten()).catch(() => {});
}
});
})();